Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b53a3a4
Added test suites: Array Buffer, Bytes, constructor-endings, construc…
HubertBer Jul 21, 2025
e5e7869
in worker & slice tests
HubertBer Jul 21, 2025
517def1
constructor dom windows + stream related tests
HubertBer Jul 21, 2025
7e847a9
stream tests
HubertBer Jul 21, 2025
47da557
slice fixes
HubertBer Jul 21, 2025
e80c2e0
Array buffer semi-fix, null type fix, byte offset fix
HubertBer Jul 21, 2025
9a6d120
array buffer fix
HubertBer Jul 21, 2025
440dbf6
removed android build files
HubertBer Jul 21, 2025
e90e2b3
Add link to the original tests on which the test suite is based
HubertBer Jul 21, 2025
245c67a
bytes method fixed
HubertBer Jul 21, 2025
5d0ce98
Blob typescript constructor changes
HubertBer Jul 21, 2025
8c14ef2
options bugfixes
HubertBer Jul 22, 2025
a90cdcd
toString changed for ExpoBlob
HubertBer Jul 22, 2025
7db8cd0
proper options handling
HubertBer Jul 22, 2025
cc73972
simpler constructor
HubertBer Jul 22, 2025
04b80e8
TypeErrors proper handling & inapplicable DOM related tests removed
HubertBer Jul 22, 2025
2eca68d
Frozen array test changed
HubertBer Jul 22, 2025
9ab8021
stream fix
HubertBer Jul 22, 2025
f859acb
newline
HubertBer Jul 22, 2025
0e5097c
constructor changes: different type checks, different mapping, differ…
HubertBer Jul 22, 2025
218ca61
blob options fix
HubertBer Jul 22, 2025
82e6c23
removed debug logs
HubertBer Jul 22, 2025
416d3ca
found the proper way to iterate over blobParts in the constructor
HubertBer Jul 23, 2025
e9f98ba
small stream fix, removed todos and console logs
HubertBer Jul 23, 2025
ad49678
Merge branch 'main' into @HubertBer/android-blob-fixes
HubertBer Jul 23, 2025
0499181
Changed Expo Blob length to match that of the default Blob
HubertBer Jul 23, 2025
bb7df98
stream fixed, removed todos (performing garbage collection in stream …
HubertBer Jul 23, 2025
4e81b3d
removed debugging logs, added ts-expect-error where necessary, added …
HubertBer Jul 23, 2025
e9504c2
fixed some tests to be more inline with the wpt tests, removed syncTe…
HubertBer Jul 23, 2025
c726574
now throwing TypeError on invalid options.endings, refactor
HubertBer Jul 23, 2025
baf9902
documentation
HubertBer Jul 23, 2025
b8fc88e
unicode bugfix
HubertBer Jul 23, 2025
cc82774
test large slice start and end values
HubertBer Jul 23, 2025
cbef234
Merge branch 'main' into @HubertBer/android-blob-fixes
HubertBer Jul 24, 2025
57323d9
performance test setup, first test
HubertBer Jul 24, 2025
b379d26
Added comparison
arturgesiarz Jul 24, 2025
90d5702
Some changes
arturgesiarz Jul 24, 2025
d0bd5cd
Temp-commit
arturgesiarz Jul 24, 2025
437fdee
Added new tests
arturgesiarz Jul 25, 2025
7d8daf9
Added conversion to base64
arturgesiarz Jul 25, 2025
a9029ce
Edited basic test
arturgesiarz Jul 25, 2025
4dc04be
Edited name BlobPerformanceTestScreen
arturgesiarz Jul 25, 2025
a73e711
BlobPerformanceScreen
arturgesiarz Jul 25, 2025
8c6bef7
Merge with main
arturgesiarz Jul 25, 2025
a56ba46
BlobPerformenceTestScreen changes
arturgesiarz Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
8 changes: 8 additions & 0 deletions apps/native-component-list/src/screens/Blob/BlobScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export const BlobScreens = [
return optionalRequire(() => require('./expo-blob/BlobBytesScreen'));
},
},
{
name: 'Blob Performance Test',
route: 'expo-blob/performance',
options: {},
getComponent() {
return optionalRequire(() => require('./expo-blob/BlobPerformanceTestScreen'));
},
},
];

export default function BlobScreen() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { Asset } from 'expo-asset';
import { ExpoBlob } from 'expo-blob';
import { useState } from 'react';
import { View, Text, StyleSheet, Button, ScrollView } from 'react-native';

import HeadingText from '../../../components/HeadingText';
import MonoText from '../../../components/MonoText';
import { Page } from '../../../components/Page';

type PerformanceTestData = {
key: string;
blobOperation: () => Promise<number>;
expoBlobOperation: () => Promise<number>;
title: string;
iterations: number;
};

function blobToBase64(blob: Blob) {
return new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result;
if (typeof result === 'string') {
resolve(result);
}
};
reader.readAsDataURL(blob);
});
}

const performanceTest: PerformanceTestData[] = [
{
key: 'basic-test',
blobOperation: async () => {
const T0 = performance.now();
const blob = new Blob(['abcd'.repeat(50000)]);
blob.slice(0, 500000).slice(0, 50_000).slice(0, 40_000).slice(0, 30_000);
const T1 = performance.now();
return T1 - T0;
},
expoBlobOperation: async () => {
const T0 = performance.now();
const blob = new ExpoBlob(['abcd'.repeat(50000)]);
blob.slice(0, 500000).slice(0, 50_000).slice(0, 40_000).slice(0, 30_000);
const T1 = performance.now();
return T1 - T0;
},
title: 'String Test',
iterations: 100,
},
{
key: 'bmp-file-test',
blobOperation: async () => {
const asset = Asset.fromModule(
require('../../../../assets/expo-blob/performance-test-2mb.bmp')
);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
const response = await fetch(uri);
const blobResponse = await response.blob();
const base64 = await blobToBase64(blobResponse);

const T0 = performance.now();
const blob = new Blob([base64]);
blob.slice(0, 1000);
const T1 = performance.now();
return T1 - T0;
},
expoBlobOperation: async () => {
const asset = Asset.fromModule(
require('../../../../assets/expo-blob/performance-test-2mb.bmp')
);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
const response = await fetch(uri);
const blobResponse = await response.blob();
const base64 = await blobToBase64(blobResponse);

const T0 = performance.now();
const blob = new ExpoBlob([base64]);
blob.slice(0, 1000);
const T1 = performance.now();
return T1 - T0;
},
title: 'File Test (2MB BMP)',
iterations: 5,
},
{
key: 'audio-file-test',
blobOperation: async () => {
const asset = Asset.fromModule(
require('../../../../assets/expo-blob/performance-test-1mb.mp3')
);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
const response = await fetch(uri);
const blobResponse = await response.blob();
const base64 = await blobToBase64(blobResponse);

const T0 = performance.now();
const blob = new Blob([base64]);
blob.slice(0, 1000);
const T1 = performance.now();
return T1 - T0;
},
expoBlobOperation: async () => {
const asset = Asset.fromModule(
require('../../../../assets/expo-blob/performance-test-1mb.mp3')
);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
const response = await fetch(uri);
const blobResponse = await response.blob();
const base64 = await blobToBase64(blobResponse);

const T0 = performance.now();
const blob = new ExpoBlob([base64]);
blob.slice(0, 1000);
const T1 = performance.now();
return T1 - T0;
},
title: 'File Test (1MB Audio)',
iterations: 25,
},
{
key: 'video-file-test',
blobOperation: async () => {
const asset = Asset.fromModule(
require('../../../../assets/expo-blob/performance-test-1mb.mp4')
);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
const response = await fetch(uri);
const blobResponse = await response.blob();
const base64 = await blobToBase64(blobResponse);

const T0 = performance.now();
const blob = new Blob([base64]);
blob.slice(0, 1000);
const T1 = performance.now();
return T1 - T0;
},
expoBlobOperation: async () => {
const asset = Asset.fromModule(
require('../../../../assets/expo-blob/performance-test-1mb.mp4')
);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
const response = await fetch(uri);
const blobResponse = await response.blob();
const base64 = await blobToBase64(blobResponse);

const T0 = performance.now();
const blob = new ExpoBlob([base64]);
blob.slice(0, 1000);
const T1 = performance.now();
return T1 - T0;
},
title: 'File Test (1MB Video)',
iterations: 25,
},
];

type ArrayBufferExampleItemProps = {
example: PerformanceTestData;
result: {
blobTime: number;
expoBlobTime: number;
} | null;
onEvaluate: (example: PerformanceTestData) => void;
};

function ArrayBufferExampleItem({ example, result, onEvaluate }: ArrayBufferExampleItemProps) {
let comparison = null;
if (result) {
const { blobTime, expoBlobTime } = result;
if (blobTime < expoBlobTime) {
const diff = expoBlobTime - blobTime;
const percent = (100 * diff) / expoBlobTime;
comparison = `Blob is ${percent.toFixed(6)}% (${diff.toFixed(6)} ms) faster`;
} else if (expoBlobTime < blobTime) {
const diff = blobTime - expoBlobTime;
const percent = (100 * diff) / blobTime;
comparison = `ExpoBlob is ${percent.toFixed(6)}% (${diff.toFixed(6)} ms) faster`;
} else {
comparison = 'Both are equally fast';
}
}
return (
<View>
<Text>{example.title}</Text>
<View>
{!result && <Button title="Evaluate" onPress={() => onEvaluate(example)} />}
{result && (
<View>
<MonoText containerStyle={styles.resultContainer}>
<Text>Blob time: {result.blobTime.toFixed(6)} ms</Text> {'\n'}
<Text>Expo Blob time: {result.expoBlobTime.toFixed(6)} ms</Text> {'\n'}
<Text>{comparison}</Text>
</MonoText>
<Button title="Re-evaluate" onPress={() => onEvaluate(example)} />
</View>
)}
</View>
</View>
);
}

export default function BlobArrayBufferScreen() {
const [results, setResults] = useState<{
[key: string]: {
blobTime: number;
expoBlobTime: number;
} | null;
}>({});

const evaluatePerformanceTest = async (example: PerformanceTestData) => {
try {
let expoBlobTotal = 0;
let blobTotal = 0;
for (let i = 0; i < example.iterations; i++) {
expoBlobTotal += await example.expoBlobOperation();
blobTotal += await example.blobOperation();
}
setResults((prev) => ({
...prev,
[example.key]: {
blobTime: blobTotal / example.iterations,
expoBlobTime: expoBlobTotal / example.iterations,
},
}));
} catch (error) {
console.error('Error in performance test', error);
setResults((prev) => ({
...prev,
[example.key]: null,
}));
}
};

return (
<Page>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.container}>
<HeadingText>Performance tests:</HeadingText>
<View style={styles.exmaplesContainer}>
{performanceTest.map((example) => (
<ArrayBufferExampleItem
key={example.key}
example={example}
result={results[example.key]}
onEvaluate={evaluatePerformanceTest}
/>
))}
</View>
</View>
</ScrollView>
</Page>
);
}

const styles = StyleSheet.create({
container: {},
scrollContainer: {
paddingBottom: 20,
},
exmaplesContainer: {
marginTop: 10,
gap: 10,
},
exampleContent: {
flexDirection: 'row',
},
resultContainer: {
borderColor: '#229D2AFF',
padding: 10,
borderRadius: 5,
},
iterationsInput: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 4,
padding: 4,
minWidth: 60,
marginLeft: 8,
},
});
30 changes: 17 additions & 13 deletions packages/expo-blob/ios/Blob.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,23 @@ public class Blob: SharedObject {
continue
}

switch part {
case .string(let str):
let utf8 = Array(str.utf8)
let subUtf8 = Array(utf8[partStart..<partEnd])
if let subStr = String(bytes: subUtf8, encoding: .utf8) {
dataSlice.append(.string(subStr))
}
case .data(let data):
let subData = data.subdata(in: partStart..<partEnd)
dataSlice.append(.data(subData))
case .blob(let blob):
let subBlob = blob.slice(start: partStart, end: partEnd, contentType: blob.type)
dataSlice.append(.blob(subBlob))
if partStart == 0 && partEnd == partSize {
dataSlice.append(part)
} else {
switch part {
case .string(let str):
let utf8 = Array(str.utf8)
let subUtf8 = Array(utf8[partStart..<partEnd])
if let subStr = String(bytes: subUtf8, encoding: .utf8) {
dataSlice.append(.string(subStr))
}
case .data(let data):
let subData = data.subdata(in: partStart..<partEnd)
dataSlice.append(.data(subData))
case .blob(let blob):
let subBlob = blob.slice(start: partStart, end: partEnd, contentType: blob.type)
dataSlice.append(.blob(subBlob))
}
}

currentPos += partSize
Expand Down