|
| 1 | +/** |
| 2 | + * What one frame of the vision pipeline costs, per model. |
| 3 | + * |
| 4 | + * Frame rate is not a comfort metric in this app: below the segmenter's floor no window can |
| 5 | + * satisfy both `minSignMs` and `minFrames`, so the app writes nothing at all. Deciding which |
| 6 | + * model to make cheaper needs its share of the budget measured on a real device, not guessed |
| 7 | + * from model file sizes — the face model is the smallest of the three on disk. |
| 8 | + */ |
| 9 | +export interface FrameCost { |
| 10 | + /** Median wall-clock milliseconds of one `detectForVideo` pass. */ |
| 11 | + readonly handsMs: number; |
| 12 | + readonly poseMs: number; |
| 13 | + readonly faceMs: number; |
| 14 | + /** How many frames the medians are taken over. */ |
| 15 | + readonly samples: number; |
| 16 | +} |
| 17 | + |
| 18 | +/** |
| 19 | + * Median of the samples taken so far, per model. |
| 20 | + * |
| 21 | + * Medians, not means: a frame that lands on a garbage collection or a lost animation frame is |
| 22 | + * several times the typical cost, and this project has already been misled once by comparing |
| 23 | + * against means over data with rare extreme outliers. |
| 24 | + */ |
| 25 | +export class FrameCostMeter { |
| 26 | + private readonly hands: number[] = []; |
| 27 | + private readonly pose: number[] = []; |
| 28 | + private readonly face: number[] = []; |
| 29 | + |
| 30 | + constructor(private readonly window = 120) {} |
| 31 | + |
| 32 | + record(handsMs: number, poseMs: number, faceMs: number): void { |
| 33 | + this.push(this.hands, handsMs); |
| 34 | + this.push(this.pose, poseMs); |
| 35 | + this.push(this.face, faceMs); |
| 36 | + } |
| 37 | + |
| 38 | + reset(): void { |
| 39 | + this.hands.length = 0; |
| 40 | + this.pose.length = 0; |
| 41 | + this.face.length = 0; |
| 42 | + } |
| 43 | + |
| 44 | + read(): FrameCost | null { |
| 45 | + if (this.hands.length === 0) return null; |
| 46 | + return { |
| 47 | + handsMs: median(this.hands), |
| 48 | + poseMs: median(this.pose), |
| 49 | + faceMs: median(this.face), |
| 50 | + samples: this.hands.length, |
| 51 | + }; |
| 52 | + } |
| 53 | + |
| 54 | + private push(into: number[], value: number): void { |
| 55 | + into.push(value); |
| 56 | + if (into.length > this.window) into.shift(); |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +function median(values: readonly number[]): number { |
| 61 | + const sorted = [...values].sort((a, b) => a - b); |
| 62 | + const middle = sorted.length >> 1; |
| 63 | + return sorted.length % 2 === 1 ? sorted[middle]! : (sorted[middle - 1]! + sorted[middle]!) / 2; |
| 64 | +} |
0 commit comments