forked from tidoust/media-tests
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathStepTimesDB.js
215 lines (199 loc) · 7.16 KB
/
StepTimesDB.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/**
* A simple in-memory database targeted at collecting step processing times for
* a set of chunks in a stream.
*
* Each entry in the database represents times for a chunk. It is an object
* with:
* - Some identifier property. By default, the code expects an "id" property.
* This can be overridden through the "chunkIdProperty" parameter passed to the
* constructor.
* - A set of properties that identify processing steps. Value each time is an
* object with "start" and "end" properties that contain recorded timestamps at
* which the step started and ended. The "end" property may not be set.
*
* For instance:
*
* {
* "id": 24000,
* "input": { "start": 1614.0, "end": 1614.0 },
* "overlay": { "start": 1614.1, "end": 1616.2 },
* "display": { "start": 1616.5 }
* }
*
* To use the db and get stats, you need to:
* 1. create an instance
* 2. add entries
* 3. call computeStats
*
* const timesDB = new StepTimesDB();
* timesDB.addEntries(timeEntries);
* const report = timesDB.computeStats();
*
* When a step only has a "start" time, the code assumes that the step ends when
* the next chunk reaches the same processing step. That logic works well with
* a "display" step in a stream of video frames where a frame replaces the
* former one. It won't work for cases where "end" should rather be the start
* time of next processing step (to handle these cases, calling code has to take
* care of setting the "end" property itself).
*
* Stats reported contain minimum, maximum, average, median and count stats for
* step durations.
*
* Stats reported also contain end-to-end statistics and the time spent in
* between processing steps, i.e. time spent in queues, provided that the names
* of the initial and final steps were given to the constructor.
*/
'use strict';
/**
* A simple in-memory database targeted at collecting processing times for
* individual chunks (video frames, typically) in a stream.
*/
class StepTimesDB {
#chunkIdProperty;
#initialStep;
#finalStep;
#excludeSteps;
#times = [];
constructor({ chunkIdProperty, initialStep, finalStep, excludeSteps } = {}) {
this.#chunkIdProperty = chunkIdProperty ?? 'id';
this.#initialStep = initialStep ?? null;
this.#finalStep = finalStep ?? null;
this.#excludeSteps = ['display'];
this.#times = [];
}
reset() {
this.#times = [];
}
find(id) {
return this.#times.find(t => t[this.#chunkIdProperty] === id);
}
addEntry(entry) {
if (!entry[this.#chunkIdProperty]) {
return;
}
const existingEntry = this.#times.find(s =>
s[this.#chunkIdProperty] === entry[this.#chunkIdProperty]);
if (existingEntry) {
Object.assign(existingEntry, entry);
}
else {
this.#times.push(entry);
}
}
addEntries(entries) {
for (const entry of entries) {
this.addEntry(entry);
}
}
/**
* Compute a report with min/max/avg/media statistics for collected times.
*/
computeStats() {
// Compute the time taken to process all chunks between given starting step
// and final step. If final step is not provided, compute the time taken by
// the starting step itself. Chunks for which we don't have the info are
// skipped.
const getDurations = (startingStep, finalStep) => {
finalStep = finalStep ?? startingStep;
return this.#times
.filter(t => t[finalStep]?.end && t[startingStep]?.start)
.map(t => t[finalStep].end - t[startingStep].start);
}
// Compute count, min, max, avg, median of the provided array of durations
const computeStats = durations => {
durations = durations.slice().sort();
const count = durations.length;
const sum = durations.reduce((sum, duration) => sum + duration, 0);
const half = count >> 1;
const median = Math.round(count % 2 === 1 ? durations[half] : (durations[half - 1] + durations[half]) / 2);
return {
count,
min: Math.round(Math.min(...durations)),
max: Math.round(Math.max(...durations)),
avg: Math.round(sum / count),
median
};
}
// Compute the time spent in between processing steps for the given chunk.
// Return null if we don't have all start/end times. Skip steps that took
// place after the final step (display is typically not included in
// "processing" time)
const computeQueuedDuration = stat => {
const finalTime = this.#finalStep ? stat[this.#finalStep]?.start : null;
const times = Object.values(stat)
.filter(time => !finalTime || time.start <= finalTime)
.sort((time1, time2) => {
const diff = time1.start - time2.start;
if (diff === 0) {
return time1.end - time2.end;
}
else {
return diff;
}
});
if (times.find(t => !t.start || !t.end)) {
return null;
}
return times.slice(0, -1)
.map((time, idx) => times[idx + 1].start - time.end)
.reduce((curr, total) => total += curr, 0);
}
// Compute a list of processing steps
const steps = new Set();
for (const stat of this.#times) {
Object.keys(stat)
.filter(step => step !== this.#chunkIdProperty)
.forEach(step => steps.add(step));
}
// If we have the start time of a step but not its end time, we'll assume
// that it lasted until the beginning of the same step for the next chunk
// (typically useful for display: we know when a frame is expected to be
// displayed, and we know it remains on display until the next frame gets
// displayed).
// TODO: the copy and sort is a bit heavy, should be done only when needed.
for (const step of steps) {
const times = this.#times.slice();
times
.sort((t1, t2) => t1[step]?.start - t2[step]?.start)
.forEach((entry, index) => {
if (entry[step]?.start && !entry[step].end) {
let nextIndex = index + 1;
let next = times[nextIndex];
while (next) {
if (next[step]?.start) {
entry[step].end = next[step].start;
break;
}
nextIndex++;
next = times[nextIndex];
}
}
});
}
const res = {
all: this.#times,
durations: this.#times.map(entry => {
const durations = { id: entry[this.#chunkIdProperty] };
for (const step of steps) {
durations[step] = entry[step]?.start && entry[step]?.end ?
Math.round(entry[step].end - entry[step].start) : 0;
}
durations.queued = computeQueuedDuration(entry);
if (this.#initialStep && this.#finalStep) {
durations.end2end = Math.round(entry[this.#finalStep]?.end - entry[this.#initialStep]?.start);
}
return durations;
}),
stats: {}
};
for (const step of steps) {
res.stats[step] = computeStats(getDurations(step));
}
const queuedDurations = this.#times.map(computeQueuedDuration).filter(s => !!s);
res.stats.queued = computeStats(queuedDurations);
if (this.#initialStep && this.#finalStep) {
res.stats.end2end = computeStats(getDurations(this.#initialStep, this.#finalStep));
}
return res;
}
}