diff --git a/ui/src/base/time.ts b/ui/src/base/time.ts index 7d4c19a690..528fdbfba9 100644 --- a/ui/src/base/time.ts +++ b/ui/src/base/time.ts @@ -25,8 +25,9 @@ export type time = Brand; // |time|s. The domain is irrelevant because a duration is relative. export type duration = bigint; -// The conversion factor for convering between time units and seconds. +// The conversion factor for converting between different time units. const TIME_UNITS_PER_SEC = 1e9; +const TIME_UNITS_PER_MILLISEC = 1e6; export class Time { // Negative time is never found in a trace - so -1 is commonly used as a flag @@ -58,20 +59,65 @@ export class Time { return v as (time | undefined); } - // Create a time from seconds. - // Use this function with caution. Number does not have the full range of time - // so only use when stricy accuracy isn't required. + // Convert seconds (number) to a time value. + // Note: number -> BigInt conversion is relatively slow. static fromSeconds(seconds: number): time { return Time.fromRaw(BigInt(Math.floor(seconds * TIME_UNITS_PER_SEC))); } // Convert time value to seconds and return as a number (i.e. float). - // Use this function with caution. Not only does it lose precision, it's also - // surpsisingly slow. Avoid using it in the render loop. + // Warning: This function is lossy, i.e. precision is lost when converting + // BigInt -> number. + // Note: BigInt -> number conversion is relatively slow. static toSeconds(t: time): number { return Number(t) / TIME_UNITS_PER_SEC; } + // Convert milliseconds (number) to a time value. + // Note: number -> BigInt conversion is relatively slow. + static fromMillis(millis: number): time { + return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MILLISEC))); + } + + // Convert time value to milliseconds and return as a number (i.e. float). + // Warning: This function is lossy, i.e. precision is lost when converting + // BigInt -> number. + // Note: BigInt -> number conversion is relatively slow. + static toMillis(t: time): number { + return Number(t) / TIME_UNITS_PER_MILLISEC; + } + + // Convert a Date object to a time value, given an offset from the unix epoch. + // Note: number -> BigInt conversion is relatively slow. + static fromDate(d: Date, offset: duration): time { + const millis = d.getTime(); + const t = Time.fromMillis(millis); + return Time.add(t, offset); + } + + // Convert time value to a Date object, given an offset from the unix epoch. + // Warning: This function is lossy, i.e. precision is lost when converting + // BigInt -> number. + // Note: BigInt -> number conversion is relatively slow. + static toDate(t: time, offset: duration): Date { + const timeSinceEpoch = Time.sub(t, offset); + const millis = Time.toMillis(timeSinceEpoch); + return new Date(millis); + } + + // Find the closest previous midnight for a given time value. + static quantWholeDaysUTC(time: time, realtimeOffset: duration): time { + const date = Time.toDate(time, realtimeOffset); + + const nearestWholeDay = new Date(Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + )); + + return Time.fromDate(nearestWholeDay, realtimeOffset); + } + static add(t: time, d: duration): time { return Time.fromRaw(t + d); } @@ -310,3 +356,12 @@ export class TimeSpan implements Span { Time.sub(this.start, padding), Time.add(this.end, padding)); } } + +// Print the date only for a given date in ISO format. +export function toISODateOnly(date: Date) { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} diff --git a/ui/src/common/timestamp_format.ts b/ui/src/common/timestamp_format.ts index b50eaed14a..08f5223d3a 100644 --- a/ui/src/common/timestamp_format.ts +++ b/ui/src/common/timestamp_format.ts @@ -17,6 +17,7 @@ export enum TimestampFormat { Raw = 'raw', RawLocale = 'rawLocale', Seconds = 'seconds', + UTC = 'utc', } let timestampFormatCached: TimestampFormat|undefined; diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts index 66f75b2b08..027398abb1 100644 --- a/ui/src/controller/trace_controller.ts +++ b/ui/src/controller/trace_controller.ts @@ -69,6 +69,7 @@ import { publishFtraceCounters, publishMetricError, publishOverviewData, + publishRealtimeOffset, publishThreads, } from '../frontend/publish'; import {runQueryInNewTab} from '../frontend/query_result_tab'; @@ -528,6 +529,65 @@ export class TraceController extends Controller { publishFtraceCounters(counters); } + { + // Find the first REALTIME or REALTIME_COARSE clock snapshot. + // Prioritize REALTIME over REALTIME_COARSE. + const query = `select + ts, + clock_value as clockValue, + clock_name as clockName + from clock_snapshot + where + snapshot_id = 0 AND + clock_name in ('REALTIME', 'REALTIME_COARSE') + `; + const result = await assertExists(this.engine).query(query); + const it = result.iter({ + ts: LONG, + clockValue: LONG, + clockName: STR, + }); + + let snapshot = { + clockName: '', + ts: Time.ZERO, + clockValue: Time.ZERO, + }; + + // Find the most suitable snapshot + for (let row = 0; it.valid(); it.next(), row++) { + if (it.clockName === 'REALTIME') { + snapshot = { + clockName: it.clockName, + ts: Time.fromRaw(it.ts), + clockValue: Time.fromRaw(it.clockValue), + }; + break; + } else if (it.clockName === 'REALTIME_COARSE') { + if (snapshot.clockName !== 'REALTIME') { + snapshot = { + clockName: it.clockName, + ts: Time.fromRaw(it.ts), + clockValue: Time.fromRaw(it.clockValue), + }; + } + } + } + + // This is the offset between the unix epoch and ts in the ts domain. + // I.e. the value of ts at the time of the unix epoch - usually some large + // negative value. + const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue); + + // Find the previous closest midnight from the trace start time. + const utcOffset = Time.quantWholeDaysUTC( + globals.state.traceTime.start, + realtimeOffset, + ); + + publishRealtimeOffset(realtimeOffset, utcOffset); + } + globals.dispatch(Actions.sortThreadTracks({})); globals.dispatch(Actions.maybeExpandOnlyTrackGroup({})); diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts index a1a8da73c3..23401a0118 100644 --- a/ui/src/frontend/app.ts +++ b/ui/src/frontend/app.ts @@ -192,6 +192,7 @@ export class App implements m.ClassComponent { async () => { const options: PromptOption[] = [ {key: TimestampFormat.Timecode, displayName: 'Timecode'}, + {key: TimestampFormat.UTC, displayName: 'Realtime (UTC)'}, {key: TimestampFormat.Seconds, displayName: 'Seconds'}, {key: TimestampFormat.Raw, displayName: 'Raw'}, { diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts index 17d8875aea..037a65f5d0 100644 --- a/ui/src/frontend/globals.ts +++ b/ui/src/frontend/globals.ts @@ -273,6 +273,8 @@ class Globals { private _ftraceCounters?: FtraceStat[] = undefined; private _ftracePanelData?: FtracePanelData = undefined; private _cmdManager?: CommandManager = undefined; + private _realtimeOffset = Time.ZERO; + private _utcOffset = Time.ZERO; // TODO(hjd): Remove once we no longer need to update UUID on redraw. private _publishRedraw?: () => void = undefined; @@ -736,16 +738,40 @@ class Globals { return assertExists(this._cmdManager); } + + // This is the ts value at the time of the Unix epoch. + // Normally some large negative value, because the unix epoch is normally in + // the past compared to ts=0. + get realtimeOffset(): time { + return this._realtimeOffset; + } + + set realtimeOffset(time: time) { + this._realtimeOffset = time; + } + + // This is the timestamp that we should use for our offset when in UTC mode. + // Usually the most recent UTC midnight compared to the trace start time. + get utcOffset(): time { + return this._utcOffset; + } + + set utcOffset(offset: time) { + this._utcOffset = offset; + } + // Offset between t=0 and the configured time domain. timestampOffset(): time { const fmt = timestampFormat(); switch (fmt) { case TimestampFormat.Timecode: case TimestampFormat.Seconds: - return globals.state.traceTime.start; + return this.state.traceTime.start; case TimestampFormat.Raw: case TimestampFormat.RawLocale: return Time.ZERO; + case TimestampFormat.UTC: + return this.utcOffset; default: const x: never = fmt; throw new Error(`Unsupported format ${x}`); diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts index 852d6944e9..7ad81ff00e 100644 --- a/ui/src/frontend/overview_timeline_panel.ts +++ b/ui/src/frontend/overview_timeline_panel.ts @@ -247,6 +247,7 @@ function renderTimestamp( ): void { const fmt = timestampFormat(); switch (fmt) { + case TimestampFormat.UTC: case TimestampFormat.Timecode: renderTimecode(ctx, time, x, y, minWidth); break; diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts index 53fa736b5f..8ed0c2ddba 100644 --- a/ui/src/frontend/publish.ts +++ b/ui/src/frontend/publish.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {time} from '../base/time'; import {Actions} from '../common/actions'; import {AggregateData, isEmptyData} from '../common/aggregation_data'; import {ConversionJobStatusUpdate} from '../common/conversion_jobs'; @@ -100,6 +101,12 @@ export function publishFtraceCounters(counters: FtraceStat[]) { globals.publishRedraw(); } +export function publishRealtimeOffset(offset: time, utcOffset: time) { + globals.realtimeOffset = offset; + globals.utcOffset = utcOffset; + globals.publishRedraw(); +} + export function publishConversionJobStatusUpdate( job: ConversionJobStatusUpdate) { globals.setConversionJobStatus(job.jobName, job.jobStatus); diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts index e76f837d61..f4372afe2e 100644 --- a/ui/src/frontend/time_axis_panel.ts +++ b/ui/src/frontend/time_axis_panel.ts @@ -17,6 +17,7 @@ import m from 'mithril'; import { Time, time, + toISODateOnly, } from '../base/time'; import {TimestampFormat, timestampFormat} from '../common/timestamp_format'; @@ -42,10 +43,21 @@ export class TimeAxisPanel extends Panel { ctx.font = '11px Roboto Condensed'; const offset = globals.timestampOffset(); - // If our timecode domain has an offset, print this offset - if (offset != 0n) { - const width = renderTimestamp(ctx, offset, 6, 10, MIN_PX_PER_STEP); - ctx.fillText('+', 6 + width + 2, 10, 6); + switch (timestampFormat()) { + case TimestampFormat.Raw: + case TimestampFormat.RawLocale: + break; + case TimestampFormat.Seconds: + case TimestampFormat.Timecode: + const width = renderTimestamp(ctx, offset, 6, 10, MIN_PX_PER_STEP); + ctx.fillText('+', 6 + width + 2, 10, 6); + break; + case TimestampFormat.UTC: + const offsetDate = + Time.toDate(globals.utcOffset, globals.realtimeOffset); + const dateStr = toISODateOnly(offsetDate); + ctx.fillText(`UTC ${dateStr}`, 6, 10); + break; } ctx.save(); @@ -59,7 +71,6 @@ export class TimeAxisPanel extends Panel { const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH); const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width); - const offset = globals.timestampOffset(); const tickGen = new TickGenerator(span, maxMajorTicks, offset); for (const {type, time} of tickGen) { if (type === TickType.MAJOR) { @@ -84,6 +95,7 @@ function renderTimestamp( ) { const fmt = timestampFormat(); switch (fmt) { + case TimestampFormat.UTC: case TimestampFormat.Timecode: return renderTimecode(ctx, time, x, y, minWidth); case TimestampFormat.Raw: diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts index 4e4fdbdd52..fcbe7c0af3 100644 --- a/ui/src/frontend/time_selection_panel.ts +++ b/ui/src/frontend/time_selection_panel.ts @@ -229,6 +229,7 @@ export class TimeSelectionPanel extends Panel { function stringifyTimestamp(time: time): string { const fmt = timestampFormat(); switch (fmt) { + case TimestampFormat.UTC: case TimestampFormat.Timecode: const THIN_SPACE = '\u2009'; return Time.toTimecode(time).toString(THIN_SPACE); diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts index ed936124e7..a4abc38252 100644 --- a/ui/src/frontend/widgets/timestamp.ts +++ b/ui/src/frontend/widgets/timestamp.ts @@ -70,6 +70,7 @@ function renderTimestamp(time: time): m.Children { const fmt = timestampFormat(); const domainTime = globals.toDomainTime(time); switch (fmt) { + case TimestampFormat.UTC: case TimestampFormat.Timecode: return renderTimecode(domainTime); case TimestampFormat.Raw: