LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_progress - query_progress_model.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 57 57
Test Date: 2026-11-06 19:30:32

            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 : }
        

Generated by: LCOV version 2.0-1