@@ -25,6 +25,17 @@ export function formatDuration(ms: number): string {
2525 return `${ ( ms / 1000 ) . toFixed ( 2 ) } s` ;
2626}
2727
28+ export function formatEta ( seconds : number ) : string {
29+ if ( ! Number . isFinite ( seconds ) || seconds < 0 ) return "—" ;
30+ const s = Math . round ( seconds ) ;
31+ const h = Math . floor ( s / 3600 ) ;
32+ const m = Math . floor ( ( s % 3600 ) / 60 ) ;
33+ const sec = s % 60 ;
34+ const pad = ( n : number ) => n . toString ( ) . padStart ( 2 , "0" ) ;
35+ if ( h > 0 ) return `${ h } :${ pad ( m ) } :${ pad ( sec ) } ` ;
36+ return `${ m } :${ pad ( sec ) } ` ;
37+ }
38+
2839export const PHASES : { key : TimingPhase ; label : string } [ ] = [
2940 { key : "dns" , label : "DNS Resolution" } ,
3041 { key : "tcp" , label : "TCP Connect" } ,
@@ -46,23 +57,27 @@ export function renderTable(title: string, rows: [string, string][]): string {
4657 return [ top , sep , ...body , bot ] . join ( "\n" ) ;
4758}
4859
60+ export type TableRow = string [ ] | "separator" ;
61+
4962export function renderMultiTable (
5063 title : string ,
5164 headers : string [ ] ,
52- rows : string [ ] [ ] ,
65+ rows : TableRow [ ] ,
5366) : string {
67+ const dataRows = rows . filter ( ( r ) : r is string [ ] => Array . isArray ( r ) ) ;
5468 const widths = headers . map ( ( h , i ) =>
55- Math . max ( h . length , ...rows . map ( ( r ) => ( r [ i ] ?? "" ) . length ) ) ,
69+ Math . max ( h . length , ...dataRows . map ( ( r ) => ( r [ i ] ?? "" ) . length ) ) ,
5670 ) ;
5771 const inner = widths . reduce ( ( a , b ) => a + b , 0 ) + ( widths . length - 1 ) * 3 + 2 ;
5872 const titleBar = ` ${ title } ` ;
5973 const top = `╭${ titleBar } ${ "─" . repeat ( Math . max ( 0 , inner - titleBar . length ) ) } ╮` ;
6074 const colSep = `├${ widths . map ( ( w ) => "─" . repeat ( w + 2 ) ) . join ( "┬" ) } ┤` ;
6175 const headerLine = `│ ${ headers . map ( ( h , i ) => h . padEnd ( widths [ i ] ! ) ) . join ( " │ " ) } │` ;
6276 const headerSep = `├${ widths . map ( ( w ) => "─" . repeat ( w + 2 ) ) . join ( "┼" ) } ┤` ;
63- const bodyLines = rows . map (
64- ( r ) => `│ ${ r . map ( ( c , i ) => ( c ?? "" ) . padEnd ( widths [ i ] ! ) ) . join ( " │ " ) } │` ,
65- ) ;
77+ const bodyLines = rows . map ( ( r ) => {
78+ if ( r === "separator" ) return headerSep ;
79+ return `│ ${ r . map ( ( c , i ) => ( c ?? "" ) . padEnd ( widths [ i ] ! ) ) . join ( " │ " ) } │` ;
80+ } ) ;
6681 const bot = `╰${ widths . map ( ( w ) => "─" . repeat ( w + 2 ) ) . join ( "┴" ) } ╯` ;
6782 return [ top , colSep , headerLine , headerSep , ...bodyLines , bot ] . join ( "\n" ) ;
6883}
@@ -80,11 +95,18 @@ export function linkInfoRows(info: LinkInformation): [string, string][] {
8095 ] ;
8196}
8297
83- export function timingRows ( results : Map < TimingPhase , number > ) : [ string , string ] [ ] {
84- return PHASES . map ( ( { key, label } ) => [
98+ export function timingRows (
99+ results : Map < TimingPhase , number > ,
100+ latencyMs ?: number | null ,
101+ ) : [ string , string ] [ ] {
102+ const rows : [ string , string ] [ ] = PHASES . map ( ( { key, label } ) => [
85103 label ,
86104 results . has ( key ) ? `${ results . get ( key ) ! . toFixed ( 2 ) } ms` : "—" ,
87105 ] ) ;
106+ if ( latencyMs !== undefined ) {
107+ rows . push ( [ "Latency" , latencyMs !== null ? `${ latencyMs . toFixed ( 2 ) } ms` : "—" ] ) ;
108+ }
109+ return rows ;
88110}
89111
90112export function downloadRows (
@@ -114,3 +136,63 @@ export function seekRows(seeks: LinkTimings[]): string[][] {
114136 fmt ( s . total ) ,
115137 ] ) ;
116138}
139+
140+ export interface SeekStat {
141+ avg : number ;
142+ stdev : number ;
143+ min : number ;
144+ max : number ;
145+ }
146+
147+ export interface SeekStats {
148+ ttfb : SeekStat | null ;
149+ receive : SeekStat | null ;
150+ total : SeekStat | null ;
151+ }
152+
153+ function statsOf ( values : number [ ] ) : SeekStat | null {
154+ if ( values . length === 0 ) return null ;
155+ const avg = values . reduce ( ( a , b ) => a + b , 0 ) / values . length ;
156+ const stdev =
157+ values . length < 2
158+ ? 0
159+ : Math . sqrt (
160+ values . reduce ( ( a , b ) => a + ( b - avg ) ** 2 , 0 ) / ( values . length - 1 ) ,
161+ ) ;
162+ return { avg, stdev, min : Math . min ( ...values ) , max : Math . max ( ...values ) } ;
163+ }
164+
165+ export function computeSeekStats ( seeks : LinkTimings [ ] ) : SeekStats {
166+ const collect = ( pick : ( s : LinkTimings ) => number | null ) =>
167+ seeks . map ( pick ) . filter ( ( v ) : v is number => v !== null ) ;
168+ return {
169+ ttfb : statsOf ( collect ( ( s ) => s . wait ) ) ,
170+ receive : statsOf ( collect ( ( s ) => s . receive ) ) ,
171+ total : statsOf ( collect ( ( s ) => s . total ) ) ,
172+ } ;
173+ }
174+
175+ export function seekStatsRows ( seeks : LinkTimings [ ] ) : TableRow [ ] {
176+ const fmt = ( n : number | null | undefined ) =>
177+ n === null || n === undefined ? "—" : `${ n . toFixed ( 2 ) } ms` ;
178+ const stats = computeSeekStats ( seeks ) ;
179+ const cols : Array < keyof SeekStat > = [ "avg" , "stdev" , "min" , "max" ] ;
180+ const labels : Record < keyof SeekStat , string > = {
181+ avg : "Avg" ,
182+ stdev : "Stdev" ,
183+ min : "Min" ,
184+ max : "Max" ,
185+ } ;
186+ const rows : TableRow [ ] = [ ] ;
187+ for ( const key of cols ) {
188+ rows . push ( "separator" ) ;
189+ rows . push ( [
190+ labels [ key ] ,
191+ "" ,
192+ fmt ( stats . ttfb ?. [ key ] ) ,
193+ fmt ( stats . receive ?. [ key ] ) ,
194+ fmt ( stats . total ?. [ key ] ) ,
195+ ] ) ;
196+ }
197+ return rows ;
198+ }
0 commit comments