Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 : import { isStatReported, type ProfileEntityState } from '../../query/profile_execution_state.js';
3 : import type { QueryProfileProgress } from '../../query/query_progress_model.js';
4 : import type { QueryExecutionProgress } from '../../query/query_execution_state.js';
5 :
6 1 : const view = dashql.buffers.profile;
7 : type ProfileStatsT = dashql.buffers.profile.ProfileStatsT;
8 :
9 : /// A statistic as number, null if not reported
10 1 : export function stat(state: ProfileEntityState | null | undefined, field: dashql.buffers.profile.ProfileStatsField, value: (stats: ProfileStatsT) => bigint): number | null {
11 1 : if (state == null || !isStatReported(state.stats, field)) return null;
12 1 : return Number(value(state.stats));
13 1 : }
14 :
15 : /// The work of an entity: completed and known units (splits for Trino)
16 : export interface ProgressWork {
17 : done: number;
18 : total: number;
19 : }
20 :
21 : /// The work of an entity, null if unknown or nothing is scheduled yet
22 1 : export function workOf(state: ProfileEntityState | null | undefined): ProgressWork | null {
23 1 : const done = stat(state, view.ProfileStatsField.WORK_DONE, s => s.workDone);
24 1 : const total = stat(state, view.ProfileStatsField.WORK_TOTAL, s => s.workTotal);
25 1 : if (done == null || total == null || total === 0) return null;
26 1 : return { done: Math.min(done, total), total };
27 1 : }
28 :
29 : /// A label of an execution status
30 : export function formatExecutionStatus(status: dashql.buffers.profile.ProfileExecutionStatus): string {
31 : switch (status) {
32 : case view.ProfileExecutionStatus.PENDING: return 'Pending';
33 : case view.ProfileExecutionStatus.RUNNING: return 'Running';
34 : case view.ProfileExecutionStatus.SUCCEEDED: return 'Done';
35 : case view.ProfileExecutionStatus.FAILED: return 'Failed';
36 : case view.ProfileExecutionStatus.SKIPPED: return 'Skipped';
37 : default: return 'Unknown';
38 : }
39 : }
40 :
41 : /// A phase name of an engine (QUEUED, WAITING_FOR_RESOURCES) as label (Queued, Waiting for resources)
42 1 : export function formatPhase(phase: string): string {
43 1 : const words = phase.toLowerCase().replace(/_/g, ' ');
44 1 : return words.charAt(0).toUpperCase() + words.slice(1);
45 1 : }
46 :
47 : /// Format a count compactly, e.g. 12, 1.3K, 2.1M, 4.2B
48 1 : export function formatCompactCount(count: number): string {
49 1 : if (count < 1000) return Math.round(count).toString();
50 1 : if (count < 1e6) return `${Number((count / 1e3).toPrecision(3))}K`;
51 1 : if (count < 1e9) return `${Number((count / 1e6).toPrecision(3))}M`;
52 1 : return `${Number((count / 1e9).toPrecision(3))}B`;
53 1 : }
54 :
55 : /// Format a duration in milliseconds, e.g. 850 ms, 1.24 s, 2m 03s, 1h 02m
56 1 : export function formatElapsed(ms: number): string {
57 1 : if (ms < 1000) return `${Math.round(ms)} ms`;
58 1 : const seconds = ms / 1000;
59 1 : if (seconds < 60) return `${Number(seconds.toPrecision(3))} s`;
60 1 : const minutes = Math.floor(seconds / 60);
61 1 : if (minutes < 60) return `${minutes}m ${Math.floor(seconds - minutes * 60).toString().padStart(2, '0')}s`;
62 1 : const hours = Math.floor(minutes / 60);
63 1 : return `${hours}h ${(minutes - hours * 60).toString().padStart(2, '0')}m`;
64 1 : }
65 :
66 : /// Format bytes compactly, e.g. 512 B, 7.8 MB
67 1 : export function formatCompactBytes(bytes: number): string {
68 1 : if (bytes < 1000) return `${Math.round(bytes)} B`;
69 1 : const units = ['kB', 'MB', 'GB', 'TB', 'PB'];
70 1 : let value = bytes / 1000;
71 1 : let unit = 0;
72 1 : while (value >= 1000 && unit < units.length - 1) {
73 1 : value /= 1000;
74 1 : unit += 1;
75 1 : }
76 1 : return `${Number(value.toPrecision(3))} ${units[unit]}`;
77 1 : }
78 :
79 : /// A short summary of the progress of a running query for its status line, e.g. `Planning` or `42% · 1.2M rows/s`.
80 : /// Null without structured progress.
81 1 : export function summarizeQueryProgress(progress: QueryExecutionProgress | null): string | null {
82 1 : const execution = progress?.profileProgress?.execution;
83 1 : if (progress == null || execution == null) return null;
84 1 : const parts: string[] = [];
85 1 : if (progress.phase != null && progress.phase !== 'RUNNING') parts.push(formatPhase(progress.phase));
86 1 : const work = workOf(execution.query);
87 1 : if (work != null) parts.push(`${Math.floor((work.done / work.total) * 100)}%`);
88 1 : const rows = stat(execution.query, view.ProfileStatsField.TUPLES_IN, s => s.tuplesIn);
89 1 : const seconds = (progress.executionTimeMs ?? 0) / 1000;
90 1 : if (rows != null && rows > 0 && seconds >= 1) parts.push(`${formatCompactCount(rows / seconds)} rows/s`);
91 1 : return parts.length === 0 ? null : parts.join(' · ');
92 1 : }
93 :
94 : /// The fraction of a progress bar that never moves backwards: Trino learns about splits while it schedules,
95 : /// so done / total can shrink. Returns the new maximum.
96 1 : export function monotonicFraction(previous: number, work: ProgressWork | null): number {
97 1 : if (work == null) return previous;
98 1 : return Math.max(previous, Math.min(1, work.done / work.total));
99 1 : }
|