waveform-analysis 0.1.48 → 0.1.50
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 +120 -5
- package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +2683 -1153
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +14 -0
- package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
- package/dist/types/components/annotation/markup.d.ts +5 -6
- package/dist/types/components/annotation/types.d.ts +2 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
- package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
- package/dist/types/components/core/types.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/useWaveformData.d.ts +2 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
- package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
- package/dist/types/components/waveform.d.ts +1 -1
- package/dist/types/core/compactSamplePointSource.d.ts +3 -0
- package/dist/types/core/data.d.ts +7 -1
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/rendering.d.ts +3 -1
- package/dist/types/core/renderingOptions.d.ts +11 -1
- package/dist/types/core/samplingReference.d.ts +36 -0
- package/dist/types/core/samplingReferenceValues.d.ts +4 -0
- package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
- package/dist/types/core/wasmSampling.d.ts +48 -0
- package/dist/types/core/waveformPointSource.d.ts +53 -0
- package/dist/types/core/workerSampling/index.d.ts +5 -0
- package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
- package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
- package/dist/types/core/workerSampling/protocol.d.ts +153 -0
- package/dist/types/core/workerSampling/repository.d.ts +36 -0
- package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
- package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
- package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
- package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/types/chart.d.ts +56 -0
- package/dist/types/types/data.d.ts +20 -2
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
- package/package.json +5 -4
- package/src/App.test.ts +11 -7
- package/src/App.vue +2 -2
- package/src/DemoRouterApp.vue +1 -0
- package/src/components/WaveformChart.css +2 -0
- package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
- package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
- package/src/components/annotation/markup.ts +45 -26
- package/src/components/annotation/types.ts +2 -0
- package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
- package/src/components/core/latestTaskScheduler.test.ts +51 -0
- package/src/components/core/latestTaskScheduler.ts +75 -0
- package/src/components/core/trackLayoutBuilder.ts +3 -0
- package/src/components/core/types.ts +2 -0
- package/src/components/core/useWaveformChartController.ts +14 -0
- package/src/components/core/useWaveformData.test.ts +95 -0
- package/src/components/core/useWaveformData.ts +23 -32
- package/src/components/core/useWaveformLayout.ts +4 -4
- package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
- package/src/components/core/useWaveformRenderSampling.ts +394 -0
- package/src/components/core/waveformChartTypes.ts +13 -0
- package/src/components/core/workerSamplingClient.ts +148 -0
- package/src/components/data/types.ts +9 -0
- package/src/components/interaction/WaveformTooltip.test.ts +1 -3
- package/src/components/interaction/WaveformTooltip.vue +13 -12
- package/src/components/interaction/useWaveformHover.ts +7 -7
- package/src/components/interaction/zoomConstraints.ts +5 -1
- package/src/components/waveform.ts +9 -0
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
- package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
- package/src/core/compactSamplePointSource.ts +76 -0
- package/src/core/data.test.ts +114 -0
- package/src/core/data.ts +177 -75
- package/src/core/index.ts +11 -0
- package/src/core/rendering.test.ts +87 -12
- package/src/core/rendering.ts +35 -1
- package/src/core/renderingOptions.ts +124 -15
- package/src/core/samplingReference.test.ts +124 -0
- package/src/core/samplingReference.ts +243 -0
- package/src/core/samplingReferenceValues.ts +169 -0
- package/src/core/samplingReferenceVectors.ts +46 -0
- package/src/core/wasmSampling.test.ts +205 -0
- package/src/core/wasmSampling.ts +230 -0
- package/src/core/waveformPointSource.ts +395 -0
- package/src/core/workerSampling/index.ts +30 -0
- package/src/core/workerSampling/javascriptBackend.ts +15 -0
- package/src/core/workerSampling/multiresolution.test.ts +69 -0
- package/src/core/workerSampling/multiresolution.ts +300 -0
- package/src/core/workerSampling/protocol.test.ts +34 -0
- package/src/core/workerSampling/protocol.ts +207 -0
- package/src/core/workerSampling/repository.test.ts +400 -0
- package/src/core/workerSampling/repository.ts +400 -0
- package/src/core/workerSampling/repositoryCache.test.ts +163 -0
- package/src/core/workerSampling/repositoryDataset.ts +194 -0
- package/src/core/workerSampling/repositorySupport.ts +225 -0
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
- package/src/core/workerSampling/samplingCache.ts +94 -0
- package/src/core/workerSampling/workerRuntime.ts +40 -0
- package/src/data/simulatedWaveforms.test.ts +19 -0
- package/src/data/simulatedWaveforms.ts +11 -8
- package/src/demoRoutes.css +177 -0
- package/src/index.ts +18 -0
- package/src/router.ts +5 -0
- package/src/types/chart.ts +63 -0
- package/src/types/data.ts +24 -1
- package/src/types/index.ts +9 -0
- package/src/views/WasmSamplingDemo.test.ts +13 -0
- package/src/views/WasmSamplingDemo.vue +180 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type { WaveformPoint } from '@/types'
|
|
2
|
+
import type { WorkerSamplingDataset } from '../waveformPointSource'
|
|
3
|
+
import { javascriptSamplingBackend } from './javascriptBackend'
|
|
4
|
+
import {
|
|
5
|
+
datasetLength,
|
|
6
|
+
datasetVisibleRange,
|
|
7
|
+
datasetXAt,
|
|
8
|
+
datasetXRange,
|
|
9
|
+
createStoredDataset,
|
|
10
|
+
disposeDatasetIndexes,
|
|
11
|
+
isWasmDatasetBackend,
|
|
12
|
+
registerWasmDataset,
|
|
13
|
+
type StoredDataset,
|
|
14
|
+
} from './repositoryDataset'
|
|
15
|
+
import {
|
|
16
|
+
type DatasetResponse,
|
|
17
|
+
type DisposeAllResponse,
|
|
18
|
+
type FindNearestPointResponse,
|
|
19
|
+
type SampleViewportResponse,
|
|
20
|
+
type WorkerSamplingBackend,
|
|
21
|
+
type WorkerSamplingBackendKind,
|
|
22
|
+
type WorkerSamplingOutput,
|
|
23
|
+
type WorkerSamplingRequest,
|
|
24
|
+
type WorkerSamplingResponse,
|
|
25
|
+
type WorkerSamplingSeriesRequest,
|
|
26
|
+
type WorkerSamplingSeriesResponse,
|
|
27
|
+
type WorkerSamplingStatus,
|
|
28
|
+
} from './protocol'
|
|
29
|
+
import { SamplingOutputCache, type SamplingCacheMetrics } from './samplingCache'
|
|
30
|
+
import {
|
|
31
|
+
cacheKey,
|
|
32
|
+
diagnosticsFor,
|
|
33
|
+
nearestPoint,
|
|
34
|
+
outputCount,
|
|
35
|
+
outputWithGlobalIndexes,
|
|
36
|
+
selectedStrategy,
|
|
37
|
+
shouldUseRaw,
|
|
38
|
+
statusFor,
|
|
39
|
+
targetPointCount,
|
|
40
|
+
} from './repositorySupport'
|
|
41
|
+
|
|
42
|
+
export interface WorkerSamplingResourceMetrics {
|
|
43
|
+
datasetCount: number
|
|
44
|
+
indexBytes: number
|
|
45
|
+
cache: SamplingCacheMetrics
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WorkerSamplingRepositoryOptions {
|
|
49
|
+
javascriptBackend?: WorkerSamplingBackend
|
|
50
|
+
wasmBackend?: WorkerSamplingBackend
|
|
51
|
+
indexMaxBytes?: number
|
|
52
|
+
cacheMaxEntries?: number
|
|
53
|
+
cacheMaxBytes?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class WorkerSamplingRepository {
|
|
57
|
+
private readonly datasets = new Map<string, StoredDataset>()
|
|
58
|
+
private readonly javascriptBackend: WorkerSamplingBackend
|
|
59
|
+
private readonly cache: SamplingOutputCache
|
|
60
|
+
private readonly indexMaxBytes: number | undefined
|
|
61
|
+
private wasmBackend: WorkerSamplingBackend | undefined
|
|
62
|
+
|
|
63
|
+
constructor(options: WorkerSamplingRepositoryOptions = {}) {
|
|
64
|
+
this.javascriptBackend = options.javascriptBackend ?? javascriptSamplingBackend
|
|
65
|
+
this.wasmBackend = options.wasmBackend
|
|
66
|
+
this.indexMaxBytes = options.indexMaxBytes
|
|
67
|
+
this.cache = new SamplingOutputCache({
|
|
68
|
+
maxEntries: options.cacheMaxEntries,
|
|
69
|
+
maxBytes: options.cacheMaxBytes,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get resourceMetrics(): WorkerSamplingResourceMetrics {
|
|
74
|
+
return {
|
|
75
|
+
datasetCount: this.datasets.size,
|
|
76
|
+
indexBytes: [...this.datasets.values()].reduce(
|
|
77
|
+
(total, dataset) => total + (dataset.wasmIndexBytes ?? dataset.index?.byteLength ?? 0),
|
|
78
|
+
0,
|
|
79
|
+
),
|
|
80
|
+
cache: this.cache.metrics,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Enables the numeric backend after the worker has initialized its WASM module. */
|
|
85
|
+
setWasmBackend(backend: WorkerSamplingBackend | undefined) {
|
|
86
|
+
if (
|
|
87
|
+
this.wasmBackend &&
|
|
88
|
+
this.wasmBackend !== backend &&
|
|
89
|
+
isWasmDatasetBackend(this.wasmBackend)
|
|
90
|
+
) {
|
|
91
|
+
this.wasmBackend.disposeAllDatasets()
|
|
92
|
+
this.datasets.forEach((dataset) => {
|
|
93
|
+
dataset.wasmDatasetHandle = undefined
|
|
94
|
+
dataset.wasmIndexBytes = undefined
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
this.wasmBackend = backend
|
|
98
|
+
if (isWasmDatasetBackend(backend))
|
|
99
|
+
this.datasets.forEach((dataset) => registerWasmDataset(backend, dataset, this.indexMaxBytes))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
handle(request: WorkerSamplingRequest): WorkerSamplingResponse {
|
|
103
|
+
switch (request.type) {
|
|
104
|
+
case 'register-dataset':
|
|
105
|
+
return this.register(request.datasetId, request.requestId, request.points, request.dataset)
|
|
106
|
+
case 'replace-dataset':
|
|
107
|
+
return this.replace(
|
|
108
|
+
request.datasetId,
|
|
109
|
+
request.requestId,
|
|
110
|
+
request.revision,
|
|
111
|
+
request.points,
|
|
112
|
+
request.dataset,
|
|
113
|
+
)
|
|
114
|
+
case 'sample-viewport':
|
|
115
|
+
return this.sample(request)
|
|
116
|
+
case 'find-nearest-point':
|
|
117
|
+
return this.findNearest(request.datasetId, request.requestId, request.revision, request.x)
|
|
118
|
+
case 'get-dataset-metrics':
|
|
119
|
+
return this.metrics(request.datasetId, request.requestId, request.revision)
|
|
120
|
+
case 'dispose-dataset':
|
|
121
|
+
return this.dispose(request.datasetId, request.requestId, request.revision)
|
|
122
|
+
case 'dispose-all':
|
|
123
|
+
return this.disposeAll(request.requestId)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private register(
|
|
128
|
+
datasetId: string,
|
|
129
|
+
requestId: number,
|
|
130
|
+
points: readonly WaveformPoint[] | undefined,
|
|
131
|
+
dataset?: WorkerSamplingDataset,
|
|
132
|
+
): DatasetResponse {
|
|
133
|
+
const existing = this.datasets.get(datasetId)
|
|
134
|
+
if (existing) return this.datasetResponse(requestId, datasetId, existing, 'stale-revision')
|
|
135
|
+
const stored = createStoredDataset(points, dataset, 1, this.indexMaxBytes)
|
|
136
|
+
registerWasmDataset(this.wasmBackend, stored, this.indexMaxBytes)
|
|
137
|
+
this.datasets.set(datasetId, stored)
|
|
138
|
+
return this.datasetResponse(requestId, datasetId, stored, 'ok')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private replace(
|
|
142
|
+
datasetId: string,
|
|
143
|
+
requestId: number,
|
|
144
|
+
revision: number,
|
|
145
|
+
points: readonly WaveformPoint[] | undefined,
|
|
146
|
+
numeric?: WorkerSamplingDataset,
|
|
147
|
+
): DatasetResponse {
|
|
148
|
+
const existing = this.datasets.get(datasetId)
|
|
149
|
+
const status = statusFor(existing, revision)
|
|
150
|
+
if (status !== 'ok' || !existing)
|
|
151
|
+
return this.datasetResponse(requestId, datasetId, existing, status)
|
|
152
|
+
disposeDatasetIndexes(this.wasmBackend, existing)
|
|
153
|
+
this.cache.deleteDataset(datasetId)
|
|
154
|
+
const dataset = createStoredDataset(points, numeric, existing.revision + 1, this.indexMaxBytes)
|
|
155
|
+
registerWasmDataset(this.wasmBackend, dataset, this.indexMaxBytes)
|
|
156
|
+
this.datasets.set(datasetId, dataset)
|
|
157
|
+
return this.datasetResponse(requestId, datasetId, dataset, 'ok')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private sample(
|
|
161
|
+
request: Extract<WorkerSamplingRequest, { type: 'sample-viewport' }>,
|
|
162
|
+
): SampleViewportResponse {
|
|
163
|
+
return {
|
|
164
|
+
type: 'sample-viewport-response',
|
|
165
|
+
requestId: request.requestId,
|
|
166
|
+
results: request.series.map((series) => this.sampleSeries(request.requestId, series)),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private sampleSeries(
|
|
171
|
+
requestId: number,
|
|
172
|
+
request: WorkerSamplingSeriesRequest,
|
|
173
|
+
): WorkerSamplingSeriesResponse {
|
|
174
|
+
const startedAt = performance.now()
|
|
175
|
+
const dataset = this.datasets.get(request.datasetId)
|
|
176
|
+
const status = statusFor(dataset, request.revision)
|
|
177
|
+
if (status !== 'ok' || !dataset) {
|
|
178
|
+
return this.sampleResponse(
|
|
179
|
+
requestId,
|
|
180
|
+
request,
|
|
181
|
+
status,
|
|
182
|
+
undefined,
|
|
183
|
+
'unavailable',
|
|
184
|
+
'sampled',
|
|
185
|
+
0,
|
|
186
|
+
startedAt,
|
|
187
|
+
false,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
const range = datasetVisibleRange(dataset, request.xDomain)
|
|
191
|
+
const visiblePointCount = range.end - range.start
|
|
192
|
+
if (shouldUseRaw(request, visiblePointCount)) {
|
|
193
|
+
return this.sampleResponse(
|
|
194
|
+
requestId,
|
|
195
|
+
request,
|
|
196
|
+
'ok',
|
|
197
|
+
{
|
|
198
|
+
kind: 'source-indexes',
|
|
199
|
+
sourceIndexes: Uint32Array.from(
|
|
200
|
+
{ length: visiblePointCount },
|
|
201
|
+
(_, index) => range.start + index,
|
|
202
|
+
),
|
|
203
|
+
},
|
|
204
|
+
'raw',
|
|
205
|
+
'raw',
|
|
206
|
+
visiblePointCount,
|
|
207
|
+
startedAt,
|
|
208
|
+
false,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
const preferredBackend =
|
|
212
|
+
request.mode === 'wasm' || (request.mode === 'auto' && this.wasmBackend)
|
|
213
|
+
? this.wasmBackend
|
|
214
|
+
: this.javascriptBackend
|
|
215
|
+
const fallbackToJavascript = !preferredBackend && request.wasmFailureFallback === 'javascript'
|
|
216
|
+
const backend = preferredBackend ?? (fallbackToJavascript ? this.javascriptBackend : undefined)
|
|
217
|
+
if (!backend) {
|
|
218
|
+
return this.sampleResponse(
|
|
219
|
+
requestId,
|
|
220
|
+
request,
|
|
221
|
+
'wasm-unavailable',
|
|
222
|
+
undefined,
|
|
223
|
+
'unavailable',
|
|
224
|
+
'sampled',
|
|
225
|
+
visiblePointCount,
|
|
226
|
+
startedAt,
|
|
227
|
+
false,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
const strategy = selectedStrategy(request.strategy)
|
|
231
|
+
const target = targetPointCount(request)
|
|
232
|
+
const key = cacheKey(request, range, strategy, target, backend.kind)
|
|
233
|
+
const cached = this.cache.get(key)
|
|
234
|
+
if (cached) {
|
|
235
|
+
return this.sampleResponse(
|
|
236
|
+
requestId,
|
|
237
|
+
request,
|
|
238
|
+
fallbackToJavascript ? 'wasm-unavailable' : 'ok',
|
|
239
|
+
cached,
|
|
240
|
+
backend.kind,
|
|
241
|
+
'sampled',
|
|
242
|
+
visiblePointCount,
|
|
243
|
+
startedAt,
|
|
244
|
+
true,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
const indexed =
|
|
248
|
+
backend.kind === 'wasm'
|
|
249
|
+
? dataset.wasmDatasetHandle !== undefined && isWasmDatasetBackend(backend)
|
|
250
|
+
? backend.sampleDataset(
|
|
251
|
+
dataset.wasmDatasetHandle,
|
|
252
|
+
range.start,
|
|
253
|
+
range.end,
|
|
254
|
+
strategy,
|
|
255
|
+
target,
|
|
256
|
+
)
|
|
257
|
+
: undefined
|
|
258
|
+
: dataset.index?.sample(range.start, range.end, strategy, target)
|
|
259
|
+
const output =
|
|
260
|
+
indexed ??
|
|
261
|
+
outputWithGlobalIndexes(
|
|
262
|
+
backend.sample({
|
|
263
|
+
x: datasetXRange(dataset, range.start, range.end),
|
|
264
|
+
y: dataset.y.subarray(range.start, range.end),
|
|
265
|
+
points: dataset.points?.slice(range.start, range.end) ?? [],
|
|
266
|
+
strategy,
|
|
267
|
+
targetPointCount: target,
|
|
268
|
+
}),
|
|
269
|
+
range.start,
|
|
270
|
+
)
|
|
271
|
+
this.cache.set(key, output)
|
|
272
|
+
if (dataset.wasmDatasetHandle !== undefined && isWasmDatasetBackend(backend)) {
|
|
273
|
+
dataset.wasmIndexBytes = backend.datasetIndexBytes(dataset.wasmDatasetHandle)
|
|
274
|
+
}
|
|
275
|
+
return this.sampleResponse(
|
|
276
|
+
requestId,
|
|
277
|
+
request,
|
|
278
|
+
fallbackToJavascript ? 'wasm-unavailable' : 'ok',
|
|
279
|
+
output,
|
|
280
|
+
backend.kind,
|
|
281
|
+
'sampled',
|
|
282
|
+
visiblePointCount,
|
|
283
|
+
startedAt,
|
|
284
|
+
false,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private sampleResponse(
|
|
289
|
+
requestId: number,
|
|
290
|
+
request: WorkerSamplingSeriesRequest,
|
|
291
|
+
status: WorkerSamplingStatus,
|
|
292
|
+
output: WorkerSamplingOutput | undefined,
|
|
293
|
+
backend: WorkerSamplingBackendKind,
|
|
294
|
+
selectedMode: 'raw' | 'sampled',
|
|
295
|
+
visiblePointCount: number,
|
|
296
|
+
startedAt: number,
|
|
297
|
+
cacheHit: boolean,
|
|
298
|
+
): WorkerSamplingSeriesResponse {
|
|
299
|
+
const diagnostics = diagnosticsFor(
|
|
300
|
+
request,
|
|
301
|
+
datasetLength(this.datasets.get(request.datasetId)),
|
|
302
|
+
visiblePointCount,
|
|
303
|
+
backend,
|
|
304
|
+
selectedMode,
|
|
305
|
+
performance.now() - startedAt,
|
|
306
|
+
outputCount(output),
|
|
307
|
+
cacheHit,
|
|
308
|
+
)
|
|
309
|
+
diagnostics.requestId = requestId
|
|
310
|
+
return {
|
|
311
|
+
seriesId: request.seriesId,
|
|
312
|
+
datasetId: request.datasetId,
|
|
313
|
+
revision: request.revision,
|
|
314
|
+
status,
|
|
315
|
+
output,
|
|
316
|
+
diagnostics,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private findNearest(
|
|
321
|
+
datasetId: string,
|
|
322
|
+
requestId: number,
|
|
323
|
+
revision: number,
|
|
324
|
+
x: number,
|
|
325
|
+
): FindNearestPointResponse {
|
|
326
|
+
const dataset = this.datasets.get(datasetId)
|
|
327
|
+
const status = statusFor(dataset, revision)
|
|
328
|
+
if (status !== 'ok' || !dataset) {
|
|
329
|
+
return { type: 'find-nearest-point-response', requestId, datasetId, revision, status }
|
|
330
|
+
}
|
|
331
|
+
const point = dataset.points
|
|
332
|
+
? nearestPoint(dataset.points, x)
|
|
333
|
+
: (() => {
|
|
334
|
+
const range = datasetVisibleRange(dataset, [x, x])
|
|
335
|
+
if (
|
|
336
|
+
!datasetLength(dataset) ||
|
|
337
|
+
x < datasetXAt(dataset, 0) ||
|
|
338
|
+
x > datasetXAt(dataset, datasetLength(dataset) - 1)
|
|
339
|
+
) {
|
|
340
|
+
return undefined
|
|
341
|
+
}
|
|
342
|
+
const right = Math.min(datasetLength(dataset) - 1, range.start)
|
|
343
|
+
const left = Math.max(0, right - 1)
|
|
344
|
+
const index =
|
|
345
|
+
Math.abs(x - datasetXAt(dataset, left)) < Math.abs(datasetXAt(dataset, right) - x)
|
|
346
|
+
? left
|
|
347
|
+
: right
|
|
348
|
+
return Number.isFinite(x) && datasetLength(dataset)
|
|
349
|
+
? { x: datasetXAt(dataset, index), y: dataset.y[index]! }
|
|
350
|
+
: undefined
|
|
351
|
+
})()
|
|
352
|
+
return { type: 'find-nearest-point-response', requestId, datasetId, revision, status, point }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private metrics(datasetId: string, requestId: number, revision: number): DatasetResponse {
|
|
356
|
+
const dataset = this.datasets.get(datasetId)
|
|
357
|
+
return this.datasetResponse(requestId, datasetId, dataset, statusFor(dataset, revision))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private dispose(datasetId: string, requestId: number, revision: number): DatasetResponse {
|
|
361
|
+
const dataset = this.datasets.get(datasetId)
|
|
362
|
+
const status = statusFor(dataset, revision)
|
|
363
|
+
if (status === 'ok' && dataset) {
|
|
364
|
+
disposeDatasetIndexes(this.wasmBackend, dataset)
|
|
365
|
+
this.cache.deleteDataset(datasetId)
|
|
366
|
+
this.datasets.delete(datasetId)
|
|
367
|
+
}
|
|
368
|
+
return this.datasetResponse(requestId, datasetId, dataset, status)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private disposeAll(requestId: number): DisposeAllResponse {
|
|
372
|
+
const disposedDatasetIds = [...this.datasets.keys()]
|
|
373
|
+
this.datasets.forEach((dataset) => disposeDatasetIndexes(this.wasmBackend, dataset))
|
|
374
|
+
if (isWasmDatasetBackend(this.wasmBackend)) this.wasmBackend.disposeAllDatasets()
|
|
375
|
+
this.datasets.clear()
|
|
376
|
+
this.cache.clear()
|
|
377
|
+
return { type: 'dispose-all-response', requestId, disposedDatasetIds }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private datasetResponse(
|
|
381
|
+
requestId: number,
|
|
382
|
+
datasetId: string,
|
|
383
|
+
dataset: StoredDataset | undefined,
|
|
384
|
+
status: WorkerSamplingStatus,
|
|
385
|
+
): DatasetResponse {
|
|
386
|
+
return {
|
|
387
|
+
type: 'dataset-response',
|
|
388
|
+
requestId,
|
|
389
|
+
datasetId,
|
|
390
|
+
revision: dataset?.revision ?? 0,
|
|
391
|
+
status,
|
|
392
|
+
metrics: dataset
|
|
393
|
+
? {
|
|
394
|
+
...dataset.metrics,
|
|
395
|
+
indexBytes: dataset.wasmIndexBytes ?? dataset.index?.byteLength ?? 0,
|
|
396
|
+
}
|
|
397
|
+
: undefined,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
})
|