Skip to content

Commit

Permalink
[ui] Add option for rendering UTC timestamps.
Browse files Browse the repository at this point in the history
Bug: 301594083
Change-Id: Ie6e768261f9ab439acb7a5abc428833e83967e44
  • Loading branch information
stevegolton committed Sep 25, 2023
1 parent 1a7f4ab commit 0941218
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 12 deletions.
67 changes: 61 additions & 6 deletions ui/src/base/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export type time = Brand<bigint, 'time'>;
// |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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -310,3 +356,12 @@ export class TimeSpan implements Span<time, duration> {
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}`;
}
1 change: 1 addition & 0 deletions ui/src/common/timestamp_format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum TimestampFormat {
Raw = 'raw',
RawLocale = 'rawLocale',
Seconds = 'seconds',
UTC = 'utc',
}

let timestampFormatCached: TimestampFormat|undefined;
Expand Down
60 changes: 60 additions & 0 deletions ui/src/controller/trace_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
publishFtraceCounters,
publishMetricError,
publishOverviewData,
publishRealtimeOffset,
publishThreads,
} from '../frontend/publish';
import {runQueryInNewTab} from '../frontend/query_result_tab';
Expand Down Expand Up @@ -528,6 +529,65 @@ export class TraceController extends Controller<States> {
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({}));

Expand Down
1 change: 1 addition & 0 deletions ui/src/frontend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
{
Expand Down
28 changes: 27 additions & 1 deletion ui/src/frontend/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
Expand Down
1 change: 1 addition & 0 deletions ui/src/frontend/overview_timeline_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions ui/src/frontend/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 17 additions & 5 deletions ui/src/frontend/time_axis_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import m from 'mithril';
import {
Time,
time,
toISODateOnly,
} from '../base/time';
import {TimestampFormat, timestampFormat} from '../common/timestamp_format';

Expand All @@ -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();
Expand All @@ -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) {
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions ui/src/frontend/time_selection_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ui/src/frontend/widgets/timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 0941218

Please sign in to comment.