waveform-analysis 0.1.63 → 0.1.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +953 -946
- package/dist/style.css +1 -1
- package/dist/types/types/chart.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/WaveformChart.vue +1 -1
- package/src/components/core/xDomain.ts +26 -2
- package/src/components/rendering/WaveformLegend.vue +1 -1
- package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
- package/src/demo/DemoChartHost.vue +1 -0
- package/src/types/chart.ts +2 -1
- package/src/App.test.ts +0 -400
- package/src/components/annotation/components.test.ts +0 -372
- package/src/components/annotation/markup.test.ts +0 -371
- package/src/components/annotation/serialization.test.ts +0 -114
- package/src/components/core/constants.test.ts +0 -29
- package/src/components/core/fixedYDomainLayout.test.ts +0 -204
- package/src/components/core/grid.test.ts +0 -197
- package/src/components/core/latestTaskScheduler.test.ts +0 -51
- package/src/components/core/layout.test.ts +0 -373
- package/src/components/core/title.test.ts +0 -76
- package/src/components/core/useWaveformData.test.ts +0 -95
- package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
- package/src/components/core/xDomain.test.ts +0 -28
- package/src/components/core/yAxisNice.test.ts +0 -174
- package/src/components/core/yAxisSlotLayout.test.ts +0 -165
- package/src/components/core/yDomain.test.ts +0 -56
- package/src/components/interaction/WaveformTooltip.test.ts +0 -276
- package/src/components/interaction/viewportInteractionState.test.ts +0 -101
- package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
- package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
- package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
- package/src/components/waveformChartCases/gridFill.test.ts +0 -95
- package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
- package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
- package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
- package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
- package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
- package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
- package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
- package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
- package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
- package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
- package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
- package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
- package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
- package/src/components/waveformLegendGeometry.test.ts +0 -21
- package/src/components/waveformNormalization.test.ts +0 -170
- package/src/core/data.test.ts +0 -241
- package/src/core/rendering.test.ts +0 -267
- package/src/core/renderingStrategies.test.ts +0 -90
- package/src/core/samplingReference.test.ts +0 -124
- package/src/core/wasmSampling.test.ts +0 -205
- package/src/core/workerSampling/multiresolution.test.ts +0 -69
- package/src/core/workerSampling/protocol.test.ts +0 -34
- package/src/core/workerSampling/repository.test.ts +0 -400
- package/src/core/workerSampling/repositoryCache.test.ts +0 -163
- package/src/core/workerSampling/repositorySampling.test.ts +0 -106
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
- package/src/data/simulatedWaveforms.test.ts +0 -60
- package/src/demo/plotMarginControls.test.ts +0 -29
- package/src/demo/xAxisLabelControls.test.ts +0 -86
- package/src/test/setup.ts +0 -82
- package/src/test/waveformChart.ts +0 -75
- package/src/utils/domain.test.ts +0 -28
- package/src/utils/formatters.test.ts +0 -131
- package/src/utils/sampling.test.ts +0 -226
- package/src/views/FixedYDomainDemo.test.ts +0 -44
- package/src/views/WasmSamplingDemo.test.ts +0 -13
- package/src/views/wasmSamplingDemoSampling.test.ts +0 -27
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import type { WaveformPoint } from '@/types'
|
|
4
|
-
import { type WorkerSamplingBackend, type WorkerSamplingSeriesRequest } from './protocol'
|
|
5
|
-
import { WorkerSamplingRepository } from './repository'
|
|
6
|
-
|
|
7
|
-
const points: WaveformPoint[] = Array.from({ length: 12 }, (_, index) => ({
|
|
8
|
-
x: index,
|
|
9
|
-
y: index === 5 ? 100 : index,
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
function register(repository: WorkerSamplingRepository, datasetId: string, source = points) {
|
|
13
|
-
return repository.handle({
|
|
14
|
-
type: 'register-dataset',
|
|
15
|
-
requestId: 1,
|
|
16
|
-
datasetId,
|
|
17
|
-
revision: 0,
|
|
18
|
-
points: source,
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function sampleSeries(datasetId: string, revision = 1): WorkerSamplingSeriesRequest {
|
|
23
|
-
return {
|
|
24
|
-
seriesId: `${datasetId}-series`,
|
|
25
|
-
datasetId,
|
|
26
|
-
revision,
|
|
27
|
-
xDomain: [0, 11],
|
|
28
|
-
plotWidth: 3,
|
|
29
|
-
mode: 'auto',
|
|
30
|
-
autoThreshold: 2,
|
|
31
|
-
strategy: 'peak',
|
|
32
|
-
maxPointsPerPixel: 2,
|
|
33
|
-
rawPointLimit: 5,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('WorkerSamplingRepository', () => {
|
|
38
|
-
it('registers an immutable normalized dataset with revision one and metrics', () => {
|
|
39
|
-
const repository = new WorkerSamplingRepository()
|
|
40
|
-
const source = [
|
|
41
|
-
{ x: 2, y: 2 },
|
|
42
|
-
{ x: Number.NaN, y: 5 },
|
|
43
|
-
{ x: 1, y: 1 },
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
const response = register(repository, 'alpha', source)
|
|
47
|
-
|
|
48
|
-
expect(response).toMatchObject({
|
|
49
|
-
type: 'dataset-response',
|
|
50
|
-
datasetId: 'alpha',
|
|
51
|
-
requestId: 1,
|
|
52
|
-
revision: 1,
|
|
53
|
-
status: 'ok',
|
|
54
|
-
metrics: { inputPointCount: 3, validPointCount: 2, xDomain: [1, 2], yDomain: [1, 2] },
|
|
55
|
-
})
|
|
56
|
-
source[0]!.x = 99
|
|
57
|
-
const nearest = repository.handle({
|
|
58
|
-
type: 'find-nearest-point',
|
|
59
|
-
requestId: 2,
|
|
60
|
-
datasetId: 'alpha',
|
|
61
|
-
revision: 1,
|
|
62
|
-
x: 2,
|
|
63
|
-
})
|
|
64
|
-
expect(nearest).toMatchObject({ status: 'ok', point: { x: 2, y: 2 } })
|
|
65
|
-
expect(
|
|
66
|
-
repository.handle({
|
|
67
|
-
type: 'get-dataset-metrics',
|
|
68
|
-
requestId: 3,
|
|
69
|
-
datasetId: 'alpha',
|
|
70
|
-
revision: 1,
|
|
71
|
-
}),
|
|
72
|
-
).toMatchObject({ status: 'ok', metrics: { validPointCount: 2 } })
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('requires the current revision for replacement and invalidates earlier queries', () => {
|
|
76
|
-
const repository = new WorkerSamplingRepository()
|
|
77
|
-
register(repository, 'alpha')
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
repository.handle({
|
|
81
|
-
type: 'replace-dataset',
|
|
82
|
-
requestId: 2,
|
|
83
|
-
datasetId: 'alpha',
|
|
84
|
-
revision: 0,
|
|
85
|
-
points: [{ x: 10, y: 10 }],
|
|
86
|
-
}),
|
|
87
|
-
).toMatchObject({ status: 'stale-revision', revision: 1 })
|
|
88
|
-
expect(
|
|
89
|
-
repository.handle({
|
|
90
|
-
type: 'replace-dataset',
|
|
91
|
-
requestId: 3,
|
|
92
|
-
datasetId: 'alpha',
|
|
93
|
-
revision: 1,
|
|
94
|
-
points: [{ x: 10, y: 10 }],
|
|
95
|
-
}),
|
|
96
|
-
).toMatchObject({ status: 'ok', revision: 2 })
|
|
97
|
-
expect(
|
|
98
|
-
repository.handle({
|
|
99
|
-
type: 'get-dataset-metrics',
|
|
100
|
-
requestId: 4,
|
|
101
|
-
datasetId: 'alpha',
|
|
102
|
-
revision: 1,
|
|
103
|
-
}),
|
|
104
|
-
).toMatchObject({ status: 'stale-revision', revision: 2 })
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('samples visible series in one request with independent auto and raw diagnostics', () => {
|
|
108
|
-
const repository = new WorkerSamplingRepository()
|
|
109
|
-
register(repository, 'sampled')
|
|
110
|
-
register(repository, 'raw')
|
|
111
|
-
const raw = {
|
|
112
|
-
...sampleSeries('raw'),
|
|
113
|
-
mode: 'raw' as const,
|
|
114
|
-
xDomain: [3, 9] as [number, number],
|
|
115
|
-
}
|
|
116
|
-
const response = repository.handle({
|
|
117
|
-
type: 'sample-viewport',
|
|
118
|
-
requestId: 5,
|
|
119
|
-
series: [sampleSeries('sampled'), raw],
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
expect(response.type).toBe('sample-viewport-response')
|
|
123
|
-
if (response.type !== 'sample-viewport-response') return
|
|
124
|
-
expect(response.results).toHaveLength(2)
|
|
125
|
-
expect(response.results[0]).toMatchObject({
|
|
126
|
-
status: 'ok',
|
|
127
|
-
output: { kind: 'source-indexes' },
|
|
128
|
-
diagnostics: {
|
|
129
|
-
mode: 'auto',
|
|
130
|
-
selectedMode: 'sampled',
|
|
131
|
-
backend: 'javascript',
|
|
132
|
-
visiblePointCount: 12,
|
|
133
|
-
requestId: 5,
|
|
134
|
-
revision: 1,
|
|
135
|
-
},
|
|
136
|
-
})
|
|
137
|
-
expect(response.results[0]?.output?.kind === 'source-indexes').toBe(true)
|
|
138
|
-
const sampled = response.results[0]?.output
|
|
139
|
-
expect(sampled?.kind === 'source-indexes' ? [...sampled.sourceIndexes] : []).toContain(5)
|
|
140
|
-
expect(response.results[1]).toMatchObject({
|
|
141
|
-
status: 'ok',
|
|
142
|
-
diagnostics: {
|
|
143
|
-
mode: 'raw',
|
|
144
|
-
selectedMode: 'raw',
|
|
145
|
-
backend: 'raw',
|
|
146
|
-
visiblePointCount: 7,
|
|
147
|
-
rawPointLimitExceeded: true,
|
|
148
|
-
},
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('returns raw source indexes at the auto threshold and does not pretend wasm is JavaScript', () => {
|
|
153
|
-
const repository = new WorkerSamplingRepository()
|
|
154
|
-
register(repository, 'alpha')
|
|
155
|
-
const atThreshold = {
|
|
156
|
-
...sampleSeries('alpha'),
|
|
157
|
-
xDomain: [2, 4] as [number, number],
|
|
158
|
-
autoThreshold: 3,
|
|
159
|
-
}
|
|
160
|
-
const forcedWasm = { ...sampleSeries('alpha'), mode: 'wasm' as const }
|
|
161
|
-
const response = repository.handle({
|
|
162
|
-
type: 'sample-viewport',
|
|
163
|
-
requestId: 6,
|
|
164
|
-
series: [atThreshold, forcedWasm],
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
expect(response.type).toBe('sample-viewport-response')
|
|
168
|
-
if (response.type !== 'sample-viewport-response') return
|
|
169
|
-
expect(response.results[0]).toMatchObject({
|
|
170
|
-
status: 'ok',
|
|
171
|
-
output: { kind: 'source-indexes', sourceIndexes: Uint32Array.from([2, 3, 4]) },
|
|
172
|
-
diagnostics: { selectedMode: 'raw', backend: 'raw', visiblePointCount: 3 },
|
|
173
|
-
})
|
|
174
|
-
expect(response.results[1]).toMatchObject({
|
|
175
|
-
status: 'wasm-unavailable',
|
|
176
|
-
output: undefined,
|
|
177
|
-
diagnostics: { selectedMode: 'sampled', backend: 'unavailable' },
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('uses JavaScript only when a forced WASM fallback explicitly allows it', () => {
|
|
182
|
-
const repository = new WorkerSamplingRepository()
|
|
183
|
-
register(repository, 'alpha')
|
|
184
|
-
const response = repository.handle({
|
|
185
|
-
type: 'sample-viewport',
|
|
186
|
-
requestId: 61,
|
|
187
|
-
series: [{ ...sampleSeries('alpha'), mode: 'wasm', wasmFailureFallback: 'javascript' }],
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
expect(response).toMatchObject({
|
|
191
|
-
type: 'sample-viewport-response',
|
|
192
|
-
results: [
|
|
193
|
-
{
|
|
194
|
-
status: 'wasm-unavailable',
|
|
195
|
-
output: { kind: 'source-indexes' },
|
|
196
|
-
diagnostics: { backend: 'javascript', selectedMode: 'sampled' },
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('uses auto hysteresis only while a previous interaction selection is supplied', () => {
|
|
203
|
-
const repository = new WorkerSamplingRepository()
|
|
204
|
-
register(repository, 'alpha')
|
|
205
|
-
const request = {
|
|
206
|
-
...sampleSeries('alpha'),
|
|
207
|
-
xDomain: [0, 3] as [number, number],
|
|
208
|
-
autoThreshold: 3,
|
|
209
|
-
autoHysteresis: 1,
|
|
210
|
-
}
|
|
211
|
-
const response = repository.handle({
|
|
212
|
-
type: 'sample-viewport',
|
|
213
|
-
requestId: 7,
|
|
214
|
-
series: [
|
|
215
|
-
{ ...request, previousSelectedMode: 'raw' },
|
|
216
|
-
{ ...request, previousSelectedMode: 'sampled' },
|
|
217
|
-
request,
|
|
218
|
-
],
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
expect(response.type).toBe('sample-viewport-response')
|
|
222
|
-
if (response.type !== 'sample-viewport-response') return
|
|
223
|
-
expect(response.results.map((result) => result.diagnostics.selectedMode)).toEqual([
|
|
224
|
-
'raw',
|
|
225
|
-
'sampled',
|
|
226
|
-
'sampled',
|
|
227
|
-
])
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('uses the default 1,000-point auto threshold per series', () => {
|
|
231
|
-
const repository = new WorkerSamplingRepository()
|
|
232
|
-
const source = Array.from({ length: 1_001 }, (_, index) => ({ x: index, y: index }))
|
|
233
|
-
register(repository, 'alpha', source)
|
|
234
|
-
const request = { ...sampleSeries('alpha'), autoThreshold: undefined, plotWidth: 100 }
|
|
235
|
-
const response = repository.handle({
|
|
236
|
-
type: 'sample-viewport',
|
|
237
|
-
requestId: 8,
|
|
238
|
-
series: [
|
|
239
|
-
{ ...request, seriesId: 'at-threshold', xDomain: [0, 999] },
|
|
240
|
-
{ ...request, seriesId: 'over-threshold', xDomain: [0, 1_000] },
|
|
241
|
-
],
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
expect(response.type).toBe('sample-viewport-response')
|
|
245
|
-
if (response.type !== 'sample-viewport-response') return
|
|
246
|
-
expect(response.results.map((result) => result.diagnostics.selectedMode)).toEqual([
|
|
247
|
-
'raw',
|
|
248
|
-
'sampled',
|
|
249
|
-
])
|
|
250
|
-
expect(response.results.map((result) => result.diagnostics.backend)).toEqual([
|
|
251
|
-
'raw',
|
|
252
|
-
'javascript',
|
|
253
|
-
])
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('uses an explicitly injected wasm backend only when forced', () => {
|
|
257
|
-
const wasmBackend: WorkerSamplingBackend = {
|
|
258
|
-
kind: 'wasm',
|
|
259
|
-
sample: () => ({ kind: 'source-indexes', sourceIndexes: Uint32Array.from([1]) }),
|
|
260
|
-
}
|
|
261
|
-
const repository = new WorkerSamplingRepository({ wasmBackend })
|
|
262
|
-
register(repository, 'alpha')
|
|
263
|
-
const response = repository.handle({
|
|
264
|
-
type: 'sample-viewport',
|
|
265
|
-
requestId: 9,
|
|
266
|
-
series: [{ ...sampleSeries('alpha'), mode: 'wasm' }],
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
expect(response).toMatchObject({
|
|
270
|
-
type: 'sample-viewport-response',
|
|
271
|
-
results: [
|
|
272
|
-
{
|
|
273
|
-
status: 'ok',
|
|
274
|
-
output: { kind: 'source-indexes', sourceIndexes: Uint32Array.from([1]) },
|
|
275
|
-
diagnostics: { backend: 'wasm' },
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
})
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('keeps TypedArray datasets numeric through the WASM request and maps local indexes globally', () => {
|
|
282
|
-
let backendRequest:
|
|
283
|
-
{ x: Float64Array; y: Float64Array; points: readonly WaveformPoint[] } | undefined
|
|
284
|
-
const repository = new WorkerSamplingRepository({
|
|
285
|
-
wasmBackend: {
|
|
286
|
-
kind: 'wasm',
|
|
287
|
-
sample: (request) => {
|
|
288
|
-
backendRequest = request
|
|
289
|
-
return { kind: 'source-indexes', sourceIndexes: Uint32Array.from([1]) }
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
})
|
|
293
|
-
const x = new Float64Array([0, 1, 2, 3])
|
|
294
|
-
const y = new Float32Array([0, 10, 20, 30])
|
|
295
|
-
repository.handle({
|
|
296
|
-
type: 'register-dataset',
|
|
297
|
-
requestId: 90,
|
|
298
|
-
datasetId: 'compact',
|
|
299
|
-
revision: 0,
|
|
300
|
-
dataset: { kind: 'typed', x, y },
|
|
301
|
-
})
|
|
302
|
-
const response = repository.handle({
|
|
303
|
-
type: 'sample-viewport',
|
|
304
|
-
requestId: 91,
|
|
305
|
-
series: [
|
|
306
|
-
{
|
|
307
|
-
...sampleSeries('compact'),
|
|
308
|
-
mode: 'wasm',
|
|
309
|
-
xDomain: [1, 3],
|
|
310
|
-
maxPointsPerPixel: 1,
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
expect(backendRequest?.x).toEqual(new Float64Array([1, 2, 3]))
|
|
316
|
-
expect(backendRequest?.y).toEqual(new Float64Array([10, 20, 30]))
|
|
317
|
-
expect(backendRequest?.points).toEqual([])
|
|
318
|
-
expect(response).toMatchObject({
|
|
319
|
-
results: [
|
|
320
|
-
{ output: { sourceIndexes: Uint32Array.from([2]) }, diagnostics: { backend: 'wasm' } },
|
|
321
|
-
],
|
|
322
|
-
})
|
|
323
|
-
expect(repository.resourceMetrics.indexBytes).toBe(0)
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('uses a WASM backend enabled after Worker initialization', () => {
|
|
327
|
-
let calls = 0
|
|
328
|
-
const repository = new WorkerSamplingRepository()
|
|
329
|
-
repository.setWasmBackend({
|
|
330
|
-
kind: 'wasm',
|
|
331
|
-
sample: () => {
|
|
332
|
-
calls += 1
|
|
333
|
-
return { kind: 'source-indexes', sourceIndexes: Uint32Array.from([2]) }
|
|
334
|
-
},
|
|
335
|
-
})
|
|
336
|
-
register(repository, 'alpha')
|
|
337
|
-
|
|
338
|
-
const response = repository.handle({
|
|
339
|
-
type: 'sample-viewport',
|
|
340
|
-
requestId: 91,
|
|
341
|
-
series: [sampleSeries('alpha')],
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
expect(calls).toBe(1)
|
|
345
|
-
expect(response).toMatchObject({
|
|
346
|
-
results: [
|
|
347
|
-
{
|
|
348
|
-
output: { sourceIndexes: Uint32Array.from([2]) },
|
|
349
|
-
diagnostics: { backend: 'wasm' },
|
|
350
|
-
},
|
|
351
|
-
],
|
|
352
|
-
})
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
it('finds nearest full-source points and disposes datasets only at the current revision', () => {
|
|
356
|
-
const repository = new WorkerSamplingRepository()
|
|
357
|
-
register(repository, 'alpha')
|
|
358
|
-
register(repository, 'beta')
|
|
359
|
-
|
|
360
|
-
expect(
|
|
361
|
-
repository.handle({
|
|
362
|
-
type: 'find-nearest-point',
|
|
363
|
-
requestId: 8,
|
|
364
|
-
datasetId: 'alpha',
|
|
365
|
-
revision: 1,
|
|
366
|
-
x: 5.2,
|
|
367
|
-
}),
|
|
368
|
-
).toMatchObject({ status: 'ok', point: points[5] })
|
|
369
|
-
expect(
|
|
370
|
-
repository.handle({
|
|
371
|
-
type: 'find-nearest-point',
|
|
372
|
-
requestId: 9,
|
|
373
|
-
datasetId: 'alpha',
|
|
374
|
-
revision: 1,
|
|
375
|
-
x: 12,
|
|
376
|
-
}),
|
|
377
|
-
).toMatchObject({ status: 'ok', point: undefined })
|
|
378
|
-
expect(
|
|
379
|
-
repository.handle({
|
|
380
|
-
type: 'dispose-dataset',
|
|
381
|
-
requestId: 10,
|
|
382
|
-
datasetId: 'alpha',
|
|
383
|
-
revision: 0,
|
|
384
|
-
}),
|
|
385
|
-
).toMatchObject({ status: 'stale-revision' })
|
|
386
|
-
expect(
|
|
387
|
-
repository.handle({
|
|
388
|
-
type: 'dispose-dataset',
|
|
389
|
-
requestId: 11,
|
|
390
|
-
datasetId: 'alpha',
|
|
391
|
-
revision: 1,
|
|
392
|
-
}),
|
|
393
|
-
).toMatchObject({ status: 'ok' })
|
|
394
|
-
expect(repository.handle({ type: 'dispose-all', requestId: 12 })).toEqual({
|
|
395
|
-
type: 'dispose-all-response',
|
|
396
|
-
requestId: 12,
|
|
397
|
-
disposedDatasetIds: ['beta'],
|
|
398
|
-
})
|
|
399
|
-
})
|
|
400
|
-
})
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import type { WaveformPoint } from '@/types'
|
|
4
|
-
import type { WorkerSamplingBackend, WorkerSamplingSeriesRequest } from './protocol'
|
|
5
|
-
import { WorkerSamplingRepository } from './repository'
|
|
6
|
-
|
|
7
|
-
const points: WaveformPoint[] = Array.from({ length: 32 }, (_, index) => ({
|
|
8
|
-
x: index,
|
|
9
|
-
y: index === 7 ? 90 : index,
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
function register(repository: WorkerSamplingRepository, datasetId: string, source = points) {
|
|
13
|
-
return repository.handle({
|
|
14
|
-
type: 'register-dataset',
|
|
15
|
-
requestId: 1,
|
|
16
|
-
datasetId,
|
|
17
|
-
revision: 0,
|
|
18
|
-
points: source,
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function request(datasetId: string, revision = 1): WorkerSamplingSeriesRequest {
|
|
23
|
-
return {
|
|
24
|
-
seriesId: `${datasetId}-series`,
|
|
25
|
-
datasetId,
|
|
26
|
-
revision,
|
|
27
|
-
xDomain: [0, 31],
|
|
28
|
-
plotWidth: 3,
|
|
29
|
-
mode: 'auto',
|
|
30
|
-
autoThreshold: 2,
|
|
31
|
-
strategy: 'peak',
|
|
32
|
-
maxPointsPerPixel: 2,
|
|
33
|
-
rawPointLimit: 5,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('WorkerSamplingRepository cache lifecycle', () => {
|
|
38
|
-
it('reports a real cache hit for a repeated viewport and misses after a rendering-key change', () => {
|
|
39
|
-
const repository = new WorkerSamplingRepository()
|
|
40
|
-
register(repository, 'alpha')
|
|
41
|
-
const series = { ...request('alpha'), lineType: 'linear' as const }
|
|
42
|
-
const first = repository.handle({ type: 'sample-viewport', requestId: 20, series: [series] })
|
|
43
|
-
const repeated = repository.handle({ type: 'sample-viewport', requestId: 21, series: [series] })
|
|
44
|
-
const changed = repository.handle({
|
|
45
|
-
type: 'sample-viewport',
|
|
46
|
-
requestId: 22,
|
|
47
|
-
series: [{ ...series, lineType: 'step-end' }],
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
expect(first).toMatchObject({ results: [{ diagnostics: { cacheHit: false } }] })
|
|
51
|
-
expect(repeated).toMatchObject({ results: [{ diagnostics: { cacheHit: true } }] })
|
|
52
|
-
expect(changed).toMatchObject({ results: [{ diagnostics: { cacheHit: false } }] })
|
|
53
|
-
expect(repository.resourceMetrics.cache.entries).toBe(2)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('invalidates cached outputs and releases indexes on revision replacement and disposal', () => {
|
|
57
|
-
const repository = new WorkerSamplingRepository()
|
|
58
|
-
register(repository, 'alpha')
|
|
59
|
-
const series = request('alpha')
|
|
60
|
-
repository.handle({ type: 'sample-viewport', requestId: 23, series: [series] })
|
|
61
|
-
expect(repository.resourceMetrics.indexBytes).toBeGreaterThan(0)
|
|
62
|
-
expect(repository.resourceMetrics.cache.entries).toBe(1)
|
|
63
|
-
|
|
64
|
-
repository.handle({
|
|
65
|
-
type: 'replace-dataset',
|
|
66
|
-
requestId: 24,
|
|
67
|
-
datasetId: 'alpha',
|
|
68
|
-
revision: 1,
|
|
69
|
-
points: points.map((point) => ({ ...point, y: point.y + 1 })),
|
|
70
|
-
})
|
|
71
|
-
expect(repository.resourceMetrics).toMatchObject({ indexBytes: 0, cache: { entries: 0 } })
|
|
72
|
-
const replacement = repository.handle({
|
|
73
|
-
type: 'sample-viewport',
|
|
74
|
-
requestId: 25,
|
|
75
|
-
series: [{ ...series, revision: 2 }],
|
|
76
|
-
})
|
|
77
|
-
expect(replacement).toMatchObject({
|
|
78
|
-
results: [{ diagnostics: { cacheHit: false, revision: 2 } }],
|
|
79
|
-
})
|
|
80
|
-
repository.handle({ type: 'dispose-dataset', requestId: 26, datasetId: 'alpha', revision: 2 })
|
|
81
|
-
expect(repository.resourceMetrics).toMatchObject({ datasetCount: 0, indexBytes: 0 })
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('uses distinct cached outputs for each series in a shared viewport request', () => {
|
|
85
|
-
const repository = new WorkerSamplingRepository()
|
|
86
|
-
register(repository, 'alpha')
|
|
87
|
-
register(
|
|
88
|
-
repository,
|
|
89
|
-
'beta',
|
|
90
|
-
points.map((point) => ({ ...point, y: -point.y })),
|
|
91
|
-
)
|
|
92
|
-
const series = [request('alpha'), request('beta')]
|
|
93
|
-
|
|
94
|
-
repository.handle({ type: 'sample-viewport', requestId: 27, series })
|
|
95
|
-
const repeated = repository.handle({ type: 'sample-viewport', requestId: 28, series })
|
|
96
|
-
|
|
97
|
-
expect(repeated).toMatchObject({
|
|
98
|
-
results: [{ diagnostics: { cacheHit: true } }, { diagnostics: { cacheHit: true } }],
|
|
99
|
-
})
|
|
100
|
-
repository.handle({ type: 'dispose-all', requestId: 29 })
|
|
101
|
-
expect(repository.resourceMetrics).toMatchObject({
|
|
102
|
-
datasetCount: 0,
|
|
103
|
-
indexBytes: 0,
|
|
104
|
-
cache: { entries: 0 },
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('computes LTTB per viewport once, then reuses only the matching viewport result', () => {
|
|
109
|
-
let calls = 0
|
|
110
|
-
const javascriptBackend: WorkerSamplingBackend = {
|
|
111
|
-
kind: 'javascript',
|
|
112
|
-
sample: ({ points }) => {
|
|
113
|
-
calls += 1
|
|
114
|
-
return { kind: 'source-indexes', sourceIndexes: Uint32Array.from([0, points.length - 1]) }
|
|
115
|
-
},
|
|
116
|
-
}
|
|
117
|
-
const repository = new WorkerSamplingRepository({ javascriptBackend })
|
|
118
|
-
register(repository, 'alpha')
|
|
119
|
-
const full = { ...request('alpha'), strategy: 'lttb' as const }
|
|
120
|
-
const zoomed = { ...full, xDomain: [4, 19] as [number, number] }
|
|
121
|
-
|
|
122
|
-
repository.handle({ type: 'sample-viewport', requestId: 30, series: [full] })
|
|
123
|
-
const repeated = repository.handle({ type: 'sample-viewport', requestId: 31, series: [full] })
|
|
124
|
-
repository.handle({ type: 'sample-viewport', requestId: 32, series: [zoomed] })
|
|
125
|
-
|
|
126
|
-
expect(calls).toBe(2)
|
|
127
|
-
expect(repeated).toMatchObject({ results: [{ diagnostics: { cacheHit: true } }] })
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('evicts least-recently-used sampled outputs at the configured entry bound', () => {
|
|
131
|
-
const repository = new WorkerSamplingRepository({ cacheMaxEntries: 1 })
|
|
132
|
-
register(repository, 'alpha')
|
|
133
|
-
const full = request('alpha')
|
|
134
|
-
const zoomed = { ...full, xDomain: [4, 19] as [number, number] }
|
|
135
|
-
|
|
136
|
-
repository.handle({ type: 'sample-viewport', requestId: 33, series: [full] })
|
|
137
|
-
repository.handle({ type: 'sample-viewport', requestId: 34, series: [zoomed] })
|
|
138
|
-
const fullAgain = repository.handle({ type: 'sample-viewport', requestId: 35, series: [full] })
|
|
139
|
-
|
|
140
|
-
expect(repository.resourceMetrics.cache).toMatchObject({ entries: 1, maxEntries: 1 })
|
|
141
|
-
expect(fullAgain).toMatchObject({ results: [{ diagnostics: { cacheHit: false } }] })
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('bounds cache memory and reports lazily allocated index bytes through dataset metrics', () => {
|
|
145
|
-
const repository = new WorkerSamplingRepository({ cacheMaxBytes: 8 })
|
|
146
|
-
register(repository, 'alpha')
|
|
147
|
-
const series = request('alpha')
|
|
148
|
-
|
|
149
|
-
repository.handle({ type: 'sample-viewport', requestId: 36, series: [series] })
|
|
150
|
-
const repeated = repository.handle({ type: 'sample-viewport', requestId: 37, series: [series] })
|
|
151
|
-
const metrics = repository.handle({
|
|
152
|
-
type: 'get-dataset-metrics',
|
|
153
|
-
requestId: 38,
|
|
154
|
-
datasetId: 'alpha',
|
|
155
|
-
revision: 1,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
expect(repeated).toMatchObject({ results: [{ diagnostics: { cacheHit: false } }] })
|
|
159
|
-
expect(repository.resourceMetrics.cache).toMatchObject({ entries: 0, maxBytes: 8 })
|
|
160
|
-
expect(metrics).toMatchObject({ status: 'ok', metrics: { indexBytes: expect.any(Number) } })
|
|
161
|
-
if (metrics.type === 'dataset-response') expect(metrics.metrics?.indexBytes).toBeGreaterThan(0)
|
|
162
|
-
})
|
|
163
|
-
})
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import type { WaveformPoint } from '@/types'
|
|
4
|
-
import type { WorkerSamplingSeriesRequest } from './protocol'
|
|
5
|
-
import { WorkerSamplingRepository } from './repository'
|
|
6
|
-
|
|
7
|
-
const points: WaveformPoint[] = Array.from({ length: 12 }, (_, index) => ({
|
|
8
|
-
x: index,
|
|
9
|
-
y: index === 5 ? 100 : index,
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
function register(repository: WorkerSamplingRepository, datasetId: string) {
|
|
13
|
-
return repository.handle({
|
|
14
|
-
type: 'register-dataset',
|
|
15
|
-
requestId: 1,
|
|
16
|
-
datasetId,
|
|
17
|
-
revision: 0,
|
|
18
|
-
points,
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function sampleSeries(datasetId: string): WorkerSamplingSeriesRequest {
|
|
23
|
-
return {
|
|
24
|
-
seriesId: `${datasetId}-series`,
|
|
25
|
-
datasetId,
|
|
26
|
-
revision: 1,
|
|
27
|
-
xDomain: [0, 11],
|
|
28
|
-
plotWidth: 1_000,
|
|
29
|
-
mode: 'auto',
|
|
30
|
-
autoThreshold: 2,
|
|
31
|
-
strategy: 'peak',
|
|
32
|
-
maxPointsPerPixel: 4,
|
|
33
|
-
maxPointCount: 4,
|
|
34
|
-
rawPointLimit: 5,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('WorkerSamplingRepository sampling targets', () => {
|
|
39
|
-
it('uses a fixed maximum point count instead of pixel density when configured', () => {
|
|
40
|
-
const repository = new WorkerSamplingRepository()
|
|
41
|
-
register(repository, 'fixed-target')
|
|
42
|
-
const response = repository.handle({
|
|
43
|
-
type: 'sample-viewport',
|
|
44
|
-
requestId: 51,
|
|
45
|
-
series: [sampleSeries('fixed-target')],
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
expect(response.type).toBe('sample-viewport-response')
|
|
49
|
-
if (response.type !== 'sample-viewport-response') return
|
|
50
|
-
expect(response.results[0]?.diagnostics.renderedPointCount).toBeLessThanOrEqual(4)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it.each(['lttb', 'average', 'sum'] as const)(
|
|
54
|
-
'returns exactly the fixed target for %s when enough points are visible',
|
|
55
|
-
(strategy) => {
|
|
56
|
-
const source = Array.from({ length: 100_000 }, (_, index) => ({
|
|
57
|
-
x: index,
|
|
58
|
-
y: Math.sin(index / 10),
|
|
59
|
-
}))
|
|
60
|
-
const repository = new WorkerSamplingRepository()
|
|
61
|
-
repository.handle({
|
|
62
|
-
type: 'register-dataset',
|
|
63
|
-
requestId: 1,
|
|
64
|
-
datasetId: strategy,
|
|
65
|
-
revision: 0,
|
|
66
|
-
points: source,
|
|
67
|
-
})
|
|
68
|
-
const response = repository.handle({
|
|
69
|
-
type: 'sample-viewport',
|
|
70
|
-
requestId: 2,
|
|
71
|
-
series: [
|
|
72
|
-
{
|
|
73
|
-
...sampleSeries(strategy),
|
|
74
|
-
xDomain: [0, 99_999],
|
|
75
|
-
strategy,
|
|
76
|
-
maxPointCount: 1_000,
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
expect(response).toMatchObject({
|
|
82
|
-
results: [{ diagnostics: { visiblePointCount: 100_000, renderedPointCount: 1_000 } }],
|
|
83
|
-
})
|
|
84
|
-
},
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
it('does not add points when the visible source has fewer than the fixed target', () => {
|
|
88
|
-
const repository = new WorkerSamplingRepository()
|
|
89
|
-
register(repository, 'small-target')
|
|
90
|
-
const response = repository.handle({
|
|
91
|
-
type: 'sample-viewport',
|
|
92
|
-
requestId: 3,
|
|
93
|
-
series: [
|
|
94
|
-
{
|
|
95
|
-
...sampleSeries('small-target'),
|
|
96
|
-
strategy: 'lttb',
|
|
97
|
-
maxPointCount: 100,
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
expect(response).toMatchObject({
|
|
103
|
-
results: [{ diagnostics: { visiblePointCount: 12, renderedPointCount: 12 } }],
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
})
|