Skip to content

Commit 50c991b

Browse files
committed
SharedArrayBuffer experiment
1 parent cc401c7 commit 50c991b

18 files changed

Lines changed: 413 additions & 23 deletions

web/angular-template.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
"serve": {
4141
"builder": "@angular/build:dev-server",
4242
"options": {
43-
"proxyConfig": "proxy.conf.mjs"
43+
"proxyConfig": "proxy.conf.mjs",
44+
"headers": {
45+
"Cross-Origin-Opener-Policy": "same-origin",
46+
"Cross-Origin-Embedder-Policy": "require-corp"
47+
}
4448
},
4549
"configurations": {
4650
"dev": {

web/karma.conf.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919

2020
module.exports = function (config) {
2121
config.set({
22+
customHeaders: [
23+
{
24+
match: '.*',
25+
name: 'Cross-Origin-Opener-Policy',
26+
value: 'same-origin',
27+
},
28+
{
29+
match: '.*',
30+
name: 'Cross-Origin-Embedder-Policy',
31+
value: 'require-corp',
32+
},
33+
],
2234
basePath: '',
2335
frameworks: ['jasmine', '@angular-devkit/build-angular'],
2436
plugins: [

web/src/app/parser/core/builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {
4646
* Collects components in a version-decoupled form.
4747
*/
4848
export class InspectionDataBuilder {
49-
private readonly internPool = new InternPoolStore();
49+
private readonly internPool = InternPoolStore.create();
5050
private readonly styleStore = new StyleStore();
5151
private readonly logStore: LogStore;
5252
private readonly timelineStore: TimelineStore;

web/src/app/services/selection-manager-v2.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('SelectionManagerV2', () => {
3737
dataStore = TestBed.inject(InspectionDataStoreV2);
3838
service = TestBed.inject(SelectionManagerV2);
3939

40-
const internPool = new InternPoolStore();
40+
const internPool = InternPoolStore.create();
4141
const styleStore = new StyleStore();
4242
logStore = new LogStore(internPool, styleStore);
4343
timelineStore = new TimelineStore(internPool, styleStore, logStore);

web/src/app/store/domain/filter/cel-env.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('CELTimelineFilterEnvironment', () => {
3535

3636
beforeEach(() => {
3737
env = new CELTimelineFilterEnvironment();
38-
internPool = new InternPoolStore();
38+
internPool = InternPoolStore.create();
3939
styleStore = new StyleStore();
4040
logStore = new LogStore(internPool, styleStore);
4141
timelineStore = new TimelineStore(internPool, styleStore, logStore);

web/src/app/store/domain/intern-pool-store.spec.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('InternPoolStore', () => {
2020
let store: InternPoolStore;
2121

2222
beforeEach(() => {
23-
store = new InternPoolStore();
23+
store = InternPoolStore.create();
2424
});
2525

2626
it('should add and get strings from the pool', () => {
@@ -62,4 +62,151 @@ describe('InternPoolStore', () => {
6262
'FieldPathSet ID 999 not found in pool',
6363
);
6464
});
65+
66+
it('should split buffer if the string size exceeds maxBufferSize', () => {
67+
const smallStore = InternPoolStore.create(10);
68+
smallStore.addStrings([
69+
{ id: 1, value: 'abcdefgh' }, // 8 bytes -> fits in 1st buffer
70+
{ id: 2, value: 'ijklmnop' }, // 8 bytes -> exceeds remaining 2 bytes, goes to 2nd buffer
71+
{ id: 3, value: 'qrstuvwxyz12345' }, // 15 bytes -> exceeds 10 bytes maxBufferSize, allocated standalone
72+
{ id: 4, value: 'abc' }, // 3 bytes -> fits in next buffer
73+
]);
74+
75+
expect(smallStore.getString(1)).toBe('abcdefgh');
76+
expect(smallStore.getString(2)).toBe('ijklmnop');
77+
expect(smallStore.getString(3)).toBe('qrstuvwxyz12345');
78+
expect(smallStore.getString(4)).toBe('abc');
79+
});
80+
81+
it('should resize metadata TypedArrays when string ID is large', () => {
82+
store.addStrings([{ id: 2000, value: 'large-id-string' }]);
83+
84+
expect(store.getString(2000)).toBe('large-id-string');
85+
});
86+
87+
it('should allow reconstructing string values from getSharedData() buffers', () => {
88+
store.addStrings([
89+
{ id: 10, value: 'shared-string-1' },
90+
{ id: 20, value: 'shared-string-2' },
91+
]);
92+
93+
const sharedData = store.getSharedData();
94+
95+
const localIndices = new Uint16Array(
96+
sharedData.metadataSab,
97+
0,
98+
sharedData.capacity,
99+
);
100+
const localOffsets = new Uint32Array(
101+
sharedData.metadataSab,
102+
sharedData.capacity * 2,
103+
sharedData.capacity,
104+
);
105+
const localLengths = new Uint32Array(
106+
sharedData.metadataSab,
107+
sharedData.capacity * 6,
108+
sharedData.capacity,
109+
);
110+
111+
const index10 = localIndices[10] - 1;
112+
const offset10 = localOffsets[10];
113+
const length10 = localLengths[10];
114+
115+
const buffer10 = new Uint8Array(sharedData.bufferSabs[index10]);
116+
const bytes10 = buffer10.subarray(offset10, offset10 + length10);
117+
const nonSharedBytes10 = new Uint8Array(length10);
118+
nonSharedBytes10.set(bytes10);
119+
const decoder = new TextDecoder();
120+
121+
expect(decoder.decode(nonSharedBytes10)).toBe('shared-string-1');
122+
123+
const index20 = localIndices[20] - 1;
124+
const offset20 = localOffsets[20];
125+
const length20 = localLengths[20];
126+
127+
const buffer20 = new Uint8Array(sharedData.bufferSabs[index20]);
128+
const bytes20 = buffer20.subarray(offset20, offset20 + length20);
129+
const nonSharedBytes20 = new Uint8Array(length20);
130+
nonSharedBytes20.set(bytes20);
131+
132+
expect(decoder.decode(nonSharedBytes20)).toBe('shared-string-2');
133+
});
134+
135+
it('should be transmissible via postMessage and decodable inside a WebWorker', (done) => {
136+
store.addStrings([{ id: 10, value: 'worker-shared-string' }]);
137+
138+
const sharedData = store.getSharedData();
139+
140+
const workerCode = `
141+
self.onmessage = function(e) {
142+
try {
143+
const sharedData = e.data;
144+
const localIndices = new Uint16Array(sharedData.metadataSab, 0, sharedData.capacity);
145+
const localOffsets = new Uint32Array(sharedData.metadataSab, sharedData.capacity * 2, sharedData.capacity);
146+
const localLengths = new Uint32Array(sharedData.metadataSab, sharedData.capacity * 6, sharedData.capacity);
147+
148+
const index = localIndices[10] - 1;
149+
const offset = localOffsets[10];
150+
const length = localLengths[10];
151+
152+
const buffer = new Uint8Array(sharedData.bufferSabs[index]);
153+
const bytes = buffer.subarray(offset, offset + length);
154+
const nonSharedBytes = new Uint8Array(length);
155+
nonSharedBytes.set(bytes);
156+
const decoded = new TextDecoder().decode(nonSharedBytes);
157+
158+
self.postMessage({ success: true, decoded });
159+
} catch (err) {
160+
self.postMessage({ success: false, error: err.toString() });
161+
}
162+
};
163+
`;
164+
const blob = new Blob([workerCode], { type: 'application/javascript' });
165+
const worker = new Worker(URL.createObjectURL(blob));
166+
167+
worker.onmessage = (e) => {
168+
const result = e.data;
169+
if (result.success) {
170+
expect(result.decoded).toBe('worker-shared-string');
171+
} else {
172+
fail('Worker failed with error: ' + result.error);
173+
}
174+
worker.terminate();
175+
done();
176+
};
177+
178+
worker.postMessage(sharedData);
179+
});
180+
181+
it('should initialize successfully from sharedData using fromSharedData()', () => {
182+
store.addStrings([
183+
{ id: 10, value: 'shared-string-1' },
184+
{ id: 20, value: 'shared-string-2' },
185+
]);
186+
store.addFieldPathSets([{ id: 100, fieldPathStringIds: [10, 20] }]);
187+
188+
const sharedData = store.getSharedData();
189+
const sharedStore = InternPoolStore.fromSharedData(sharedData);
190+
191+
expect(sharedStore.getString(10)).toBe('shared-string-1');
192+
expect(sharedStore.getString(20)).toBe('shared-string-2');
193+
expect(sharedStore.getFieldPathSet(100)).toEqual([
194+
'shared-string-1',
195+
'shared-string-2',
196+
]);
197+
});
198+
199+
it('should throw an error on write attempts on a read-only instance created from sharedData', () => {
200+
store.addStrings([{ id: 10, value: 'shared-string' }]);
201+
const sharedData = store.getSharedData();
202+
const sharedStore = InternPoolStore.fromSharedData(sharedData);
203+
204+
expect(() => {
205+
sharedStore.addStrings([{ id: 11, value: 'fail' }]);
206+
}).toThrowError('Cannot write to a shared read-only InternPoolStore');
207+
208+
expect(() => {
209+
sharedStore.addFieldPathSets([{ id: 200, fieldPathStringIds: [10] }]);
210+
}).toThrowError('Cannot write to a shared read-only InternPoolStore');
211+
});
65212
});

0 commit comments

Comments
 (0)