summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/utils/perf.ts
blob: 0ecbc33ffc1e00563b6d421bca2f53be5f6d701c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
type Nav = {
  id: string
  dir?: string
  from?: string
  to: string
  trigger?: string
  start: number
  marks: Record<string, number>
  logged: boolean
  timer?: ReturnType<typeof setTimeout>
}

const dev = import.meta.env.DEV

const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`

const now = () => performance.now()

const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)

const navs = new Map<string, Nav>()
const pending = new Map<string, string>()
const active = new Map<string, string>()

const required = [
  "session:params",
  "session:data-ready",
  "session:first-turn-mounted",
  "storage:prompt-ready",
  "storage:terminal-ready",
  "storage:file-view-ready",
]

function flush(id: string, reason: "complete" | "timeout") {
  if (!dev) return
  const nav = navs.get(id)
  if (!nav) return
  if (nav.logged) return

  nav.logged = true
  if (nav.timer) clearTimeout(nav.timer)

  const baseName = nav.marks["navigate:start"] !== undefined ? "navigate:start" : "session:params"
  const base = nav.marks[baseName] ?? nav.start

  const ms = Object.fromEntries(
    Object.entries(nav.marks)
      .slice()
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([name, t]) => [name, Math.round((t - base) * 100) / 100]),
  )

  console.log(
    "perf.session-nav " +
      JSON.stringify({
        type: "perf.session-nav.v0",
        id: nav.id,
        dir: nav.dir,
        from: nav.from,
        to: nav.to,
        trigger: nav.trigger,
        base: baseName,
        reason,
        ms,
      }),
  )

  navs.delete(id)
}

function maybeFlush(id: string) {
  if (!dev) return
  const nav = navs.get(id)
  if (!nav) return
  if (nav.logged) return
  if (!required.every((name) => nav.marks[name] !== undefined)) return
  flush(id, "complete")
}

function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
  const existing = navs.get(id)
  if (existing) return existing

  const nav: Nav = {
    ...data,
    marks: {},
    logged: false,
  }
  nav.timer = setTimeout(() => flush(id, "timeout"), 5000)
  navs.set(id, nav)
  return nav
}

export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
  if (!dev) return

  const id = uid()
  const start = now()
  const nav = ensure(id, { ...input, id, start })
  nav.marks["navigate:start"] = start

  pending.set(key(input.dir, input.to), id)
  return id
}

export function navParams(input: { dir?: string; from?: string; to: string }) {
  if (!dev) return

  const k = key(input.dir, input.to)
  const pendingId = pending.get(k)
  if (pendingId) pending.delete(k)
  const id = pendingId ?? uid()

  const start = now()
  const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
  nav.marks["session:params"] = start

  active.set(k, id)
  maybeFlush(id)
  return id
}

export function navMark(input: { dir?: string; to: string; name: string }) {
  if (!dev) return

  const id = active.get(key(input.dir, input.to))
  if (!id) return

  const nav = navs.get(id)
  if (!nav) return
  if (nav.marks[input.name] !== undefined) return

  nav.marks[input.name] = now()
  maybeFlush(id)
}