Compare commits

..

6 Commits

Author SHA1 Message Date
1f0c7bd099 Remove try-catch from perfToggle and fix perf timing flag read
Let errors propagate to the caller instead of catching locally,
simplify the request body construction, and snapshot perf.timing
before the fetch to avoid a TOCTOU race.
2026-03-19 11:50:10 -07:00
4cc0ff2bed Inline PerfState interface and move subscriptions before routes
The PerfState interface was only used twice, so inline it. Move
onChange/onHostLog subscriptions above the route definitions to keep
side-effects grouped. Skip perf.now() in proxy when timing is off.
2026-03-19 11:29:17 -07:00
9a19c0a861 Consolidate perfTiming state into a single perf object
Removes the separate variable and setter in favor of a plain object,
making the mutable state easier to track and eliminating a needless
abstraction.
2026-03-19 11:21:50 -07:00
b9f94a6c98 Move setPerfTiming next to perfTiming and always capture request start time
Colocate the setter with its variable for readability. Remove the
conditional around performance.now() since the call is negligible
and simplifies the timing logic.
2026-03-19 11:09:27 -07:00
c42c73fe70 Simplify perf toggle by deduplicating branching logic
Early-return on invalid input, then unify the GET/POST and display
paths so each concern is handled once instead of per-subcommand.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:02:06 -07:00
c3ad78f1be Add CLI perf command to toggle request timing on proxied apps 2026-03-19 10:50:23 -07:00
5 changed files with 48 additions and 2 deletions

View File

@ -16,3 +16,4 @@ export {
unshareApp, unshareApp,
} from './manage' } from './manage'
export { metricsApp } from './metrics' export { metricsApp } from './metrics'
export { perfToggle } from './perf'

17
src/cli/commands/perf.ts Normal file
View File

@ -0,0 +1,17 @@
import color from 'ansis'
import { get, post } from '../http'
export async function perfToggle(onOff?: string) {
if (onOff && !['on', 'off', 'status'].includes(onOff)) {
console.error('Usage: toes perf [on|off|status]')
process.exit(1)
}
const body = onOff && onOff !== 'status' ? { on: onOff === 'on' } : {}
const res = onOff === 'status'
? await get<{ perfTiming: boolean }>('/api/system/perf')
: await post<{ perfTiming: boolean }>('/api/system/perf', body)
if (res) {
const label = res.perfTiming ? color.green('on') : color.red('off')
console.log(`Perf timing ${onOff === 'status' ? 'is ' : ''}${label}`)
}
}

View File

@ -16,14 +16,15 @@ import {
infoApp, infoApp,
listApps, listApps,
logApp, logApp,
metricsApp,
newApp, newApp,
openApp, openApp,
perfToggle,
renameApp, renameApp,
restartApp, restartApp,
rmApp, rmApp,
shareApp, shareApp,
startApp, startApp,
metricsApp,
stopApp, stopApp,
unshareApp, unshareApp,
} from './commands' } from './commands'
@ -232,6 +233,13 @@ env
.option('-g, --global', 'remove a global variable') .option('-g, --global', 'remove a global variable')
.action(envRm) .action(envRm)
program
.command('perf')
.helpGroup('Config:')
.description('Toggle request timing for proxied app requests')
.argument('[on|off|status]', 'enable, disable, or check status (toggles if omitted)')
.action(perfToggle)
// Shell // Shell
program program

View File

@ -1,4 +1,5 @@
import { allApps, APPS_DIR, onChange } from '$apps' import { allApps, APPS_DIR, onChange } from '$apps'
import { perf } from '../proxy'
import { onHostLog } from '../tui' import { onHostLog } from '../tui'
import { Hype } from '@because/hype' import { Hype } from '@because/hype'
import { cpus, freemem, platform, totalmem } from 'os' import { cpus, freemem, platform, totalmem } from 'os'
@ -283,6 +284,16 @@ onChange(collectLogs)
// Subscribe to host-level log messages // Subscribe to host-level log messages
onHostLog(pushHostLog) onHostLog(pushHostLog)
// Perf timing toggle
router.get('/perf', c => c.json({ perfTiming: perf.timing }))
router.post('/perf', async c => {
const body = await c.req.json<{ on?: boolean }>().catch(() => ({}))
const on = body.on ?? !perf.timing
perf.timing = on
console.log(`[perf] timing ${on ? 'enabled' : 'disabled'}`)
return c.json({ perfTiming: on })
})
// Restart server (systemd brings it back) // Restart server (systemd brings it back)
router.post('/restart', c => { router.post('/restart', c => {
setTimeout(() => process.exit(0), 100) setTimeout(() => process.exit(0), 100)

View File

@ -1,6 +1,8 @@
import type { Server, ServerWebSocket } from 'bun' import type { Server, ServerWebSocket } from 'bun'
import { getAppBySubdomain } from '$apps' import { getAppBySubdomain } from '$apps'
export const perf = { timing: false }
export type { WsData } export type { WsData }
const pendingMessages = new Map<ServerWebSocket<WsData>, (string | ArrayBuffer | Uint8Array)[]>() const pendingMessages = new Map<ServerWebSocket<WsData>, (string | ArrayBuffer | Uint8Array)[]>()
@ -53,12 +55,19 @@ export async function proxySubdomain(subdomain: string, req: Request): Promise<R
headers.delete('transfer-encoding') headers.delete('transfer-encoding')
try { try {
return await fetch(target, { const shouldTime = perf.timing
const start = shouldTime ? performance.now() : 0
const res = await fetch(target, {
method: req.method, method: req.method,
headers, headers,
body, body,
redirect: 'manual', redirect: 'manual',
}) })
if (shouldTime) {
const ms = (performance.now() - start).toFixed(1)
console.log(`[perf] ${req.method} ${subdomain}${url.pathname}${res.status} in ${ms}ms`)
}
return res
} catch (e) { } catch (e) {
console.error(`Proxy error for ${subdomain}:`, e) console.error(`Proxy error for ${subdomain}:`, e)
return new Response(`App "${subdomain}" is not responding`, { status: 502 }) return new Response(`App "${subdomain}" is not responding`, { status: 502 })