Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_ENGINE_ADDRESS=http://localhost:8080
SERVER_PORT=8080 # passed to benchttp-server, must match VITE_ENGINE_ADDRESS
5 changes: 4 additions & 1 deletion scripts/sidecar-exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ target_triple=$(rustc -Vv | grep host | cut -f2 -d' ')
# The binary is suffixed with the triple target as required by tauri sidecar api.
program="benchttp-server-$target_triple"

./src-tauri/bin/$program
# Run with .env variables loaded.
flag="--auto-port=false"

./src-tauri/bin/$program $flag
6 changes: 1 addition & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { useSpawnEngine } from '@/hooks'

import './App.css'
import { SimpleStream } from './examples'

function App() {
const { isLoading } = useSpawnEngine()

return (
<div className="App">
<h1>Benchttp</h1>
{isLoading ? <div>Loading</div> : <SimpleStream />}
<SimpleStream />
</div>
)
}
Expand Down
96 changes: 82 additions & 14 deletions src/engine/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,92 @@ const program = 'benchttp-server'

const command = Command.sidecar(`bin/${program}`)

export const spawnEngine = async () => {
command.on('close', (data) =>
console.log(
`${program} finished with code ${data.code} and signal ${data.signal}`
/**
* Spawns benchttp-server as a child process and returns a Promise
* that will resolve with the address the server is listening on,
* or reject if the process closes or produces an unexpected error.
*/
const spawnEngine = async (): Promise<string> => {
const child = await command.spawn()

return new Promise((resolve, reject) => {
command.on('close', (data) => {
const { code, signal } = data
console.log(`${program} finished with code ${code} and signal ${signal}`)
reject({ message: `${program} closed unexpectedly`, code, signal })
})

command.on('error', (error) => {
console.error(`${program} error: "${error}"`)
reject({ message: `${program} error`, error })
})

command.stdout.on('data', (line) => {
// Wait for the ready signal line before resolving the enclosing Promise.
resolveAddr(resolve, line)

console.log(`${program} stdout: "${line}"`)
})

command.stderr.on('data', (line) =>
console.error(`${program} stderr: "${line}"`)
)
)

command.on('error', (error) => console.error(`${program} error: "${error}"`))
console.log(`${program} pid: ${child.pid}`)
})
}

command.stdout.on('data', (line) =>
console.log(`${program} stdout: "${line}"`)
)
const readySignal = 'READY'

command.stderr.on('data', (line) =>
console.error(`${program} stderr: "${line}"`)
)
const host = 'localhost'

const child = await command.spawn()
type ReadySignalLine = `${typeof readySignal} http://${typeof host}:${number}`

const isReadySignal = (line: unknown): line is ReadySignalLine =>
typeof line === 'string' && line.startsWith(readySignal)

// Expect the engine to always respect the ready signal contract.
// Then we know the address is index 1.
const getAddr = (line: ReadySignalLine): string => line.split(' ')[1]

type PromiseResolve<T> = (value: T | PromiseLike<T>) => void

const resolveAddr = (resolve: PromiseResolve<string>, line: unknown): void => {
isReadySignal(line) && resolve(getAddr(line))
}

/**
* Spawns benchttp-server as a child process.
*
* If called in the browser, `mustSpawnEngine` will read
* the address to use from the current environment.
*
* If the address cannot be resolved, throws an exception.
*/
export async function mustSpawnEngine(): Promise<void> {
let addr = ''
if (isWeb()) {
console.warn(
`Running in the browser: cannot spawn sidecar with @tauri-apps/api/shell. Make sure engine is running:
npm run sidecar:exec`
)
addr = import.meta.env.VITE_ENGINE_ADDRESS
}
addr = await spawnEngine()

setAddress(addr)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type WindowTauri = typeof window & { __TAURI__: any }

const isWeb = () => (window as WindowTauri).__TAURI__ === undefined

/**
* The address at which benchttp-server is listening.
*/
export let address: string

console.log(`${program} pid: ${child.pid}`)
function setAddress(value: string): void {
address = value
}
18 changes: 12 additions & 6 deletions src/engine/stream.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { RunProgress, RunReport, RunError, RunConfiguration } from '@/benchttp'

const streamUrl = 'http://localhost:8080/stream'

export type RunStream = ProgressStream | ReportStream | ErrorStream

interface ProgressStream {
Expand All @@ -28,7 +26,7 @@ export class RunStreamer {
#canceled = false
#aborter = new AbortController()

constructor(private emit: RunStreamerOptions) {}
constructor(private address: string, private emit: RunStreamerOptions) {}

/**
* Sends a run request to the local benchttp server with the input config,
Expand All @@ -42,7 +40,11 @@ export class RunStreamer {
this.#aborter = new AbortController()

try {
const reader = await startRunStream(config, this.#aborter.signal)
const reader = await startRunStream(
config,
this.address,
this.#aborter.signal
)

while (!this.#canceled) {
const { done, value } = await reader.read()
Expand Down Expand Up @@ -76,22 +78,26 @@ const runStreamDecoder = () =>
const runStreamWriter = (write: (s: RunStream) => void) =>
new WritableStream({ write })

const streamUrl = (address: string) => `${address}/stream`

/**
* Makes the HTTP request to start a run and returns a ReadableStream
* to stream the emitted progress and report data.
*/
const startRunStream = async (
config: RunConfiguration,
address: string,
signal: AbortSignal
) => {
const response = await fetch(streamUrl, {
const url = streamUrl(address)
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(config),
signal,
})
const reader = response?.body?.getReader()

if (!reader) throw new Error(`Cannot fetch ${streamUrl}`)
if (!reader) throw new Error(`Cannot fetch ${url}`)
return reader
}

Expand Down
2 changes: 1 addition & 1 deletion src/examples/SimpleStream/SimpleStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useRunStream } from '@/hooks'
import { inputConfig } from '../inputConfig'
import { RunControlPanel, RunDisplay } from './components'

export const SimpleStream = () => {
export const SimpleStream: React.FC = () => {
const { start, stop, reset, progress, report, error } = useRunStream()

return (
Expand Down
1 change: 0 additions & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { useRunStream } from './useRunStream'
export { useSpawnEngine } from './useSpawnEngine'
15 changes: 0 additions & 15 deletions src/hooks/useOnce.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/hooks/useRunStream.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef, useReducer } from 'react'

import { RunProgress, RunReport } from '@/benchttp'
import { address } from '@/engine/spawn'
import { RunStreamer, RunStream } from '@/engine/stream'

interface State {
Expand Down Expand Up @@ -43,7 +44,7 @@ export function useRunStream() {
const [state, dispatch] = useReducer(reducer, initState())

const stream = useRef(
new RunStreamer({
new RunStreamer(address, {
onError: (err) => dispatch(['ERROR', err.message]),
onStream: (stream) => dispatch(['STREAM', stream]),
})
Expand Down
41 changes: 0 additions & 41 deletions src/hooks/useSpawnEngine.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './App'
import { mustSpawnEngine } from './engine/spawn'
import './index.css'

await mustSpawnEngine()

// eslint-disable-next-line import/no-named-as-default-member
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
Expand Down
8 changes: 8 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_ENGINE_ADDRESS: string
}

interface ImportMeta {
readonly env: ImportMetaEnv
}
1 change: 1 addition & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig({
build: {
target: ['esnext'],
},
envPrefix: ['VITE_', 'TAURI_'],
resolve: {
alias: {
'@': path.resolve('./src'),
Expand Down