Skip to content

Commit

Permalink
add a simple "load test" example
Browse files Browse the repository at this point in the history
  • Loading branch information
pschichtel committed Dec 5, 2024
1 parent ce53839 commit 381aca2
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 20 deletions.
49 changes: 49 additions & 0 deletions loadtest-example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>WebRTC Reference Client</title>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" type="text/css" href="index.css">
<link rel="stylesheet" type="text/css" href="webaudio.css">
<script type="module" src="dist/loadtest-example-bundle.js"></script>
</head>
<body>
<main>
<h1>
VIER Cognitive Voice Gateway<br>
WebRTC Reference Client
</h1>
<section>
<div class="form-field">
<div>
<label for="environment">Environment</label>
</div>
<div>
<input id="environment" type="text" readonly disabled>
</div>
</div>
<div class="form-field">
<div>
<label for="reseller-token">Reseller Token</label>
</div>
<div>
<input id="reseller-token" name="reseller-token" type="text">
</div>
</div>
<div class="form-field">
<div>
<label for="destination">Destination</label>
</div>
<div>
<input id="destination" name="destination" type="tel">
</div>
</div>
<div>
<button id="start-calls">Start calls!</button>
</div>
</section>
</main>
</body>
</html>

67 changes: 67 additions & 0 deletions src/common-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,71 @@ export function concurrencyLimitedWorkQueue<T>(maxConcurrency: number): WorkQueu
return new Promise(resolve => emptyPromises.push(resolve))
}
}
}

export function getDialogId(headers: HeaderList): string | undefined {
const dialogIds = headers
.filter(([name,]) => name.toLowerCase() == "x-vier-dialogid")
.map(([, value]) => value)
if (dialogIds.length > 0) {
return dialogIds[0]
}
return undefined
}

export interface BaseDialogDataEntry {
type: string,
timestamp: number
}

export interface StartDialogDataEntry extends BaseDialogDataEntry {
type: "Start"
customSipHeaders: {[name: string]: Array<string>}
}

export interface SynthesisDialogDataEntry extends BaseDialogDataEntry {
type: "Synthesis"
text: string
plainText: string
vendor: string
language: string
}

export interface ToneDialogDataEntry extends BaseDialogDataEntry {
type: "Tone"
tone: string
triggeredBargeIn: boolean
}

export interface TranscriptionDialogDataEntry extends BaseDialogDataEntry {
type: "Transcription"
text: string
confidence: number
vendor: string
language: string
triggeredBargeIn: boolean
}

export interface EndDialogDataEntry extends BaseDialogDataEntry {
type: "End"
reason: string
}

export type DialogDataEntry = StartDialogDataEntry | SynthesisDialogDataEntry | ToneDialogDataEntry | TranscriptionDialogDataEntry | EndDialogDataEntry | BaseDialogDataEntry

export interface DialogDataResponse {
dialogId: string
callId?: string
data: Array<DialogDataEntry>
}

export async function fetchDialogData(environment: string, resellerToken: string, dialogId: string) {
const response = await fetch(`${environment}/v1/dialog/${resellerToken}/${dialogId}`)
return await response.json() as DialogDataResponse
}

export function delay(millis: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, millis)
})
}
133 changes: 133 additions & 0 deletions src/loadtest-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
CreateCallOptions,
DEFAULT_ICE_GATHERING_TIMEOUT,
fetchWebRtcAuthDetails,
setupSipClient,
} from './client'
import {
DEFAULT_TIMEOUT,
enableMediaStreamAudioInChrome,
} from './controls'
import {
concurrencyLimitedWorkQueue,
delay,
fetchDialogData,
getAndDisplayEnvironmentFromQuery,
getDialogId,
updateQueryParameter,
} from './common-example'

async function performCall(
environment: string,
resellerToken: string,
destination: string,
audioContext: AudioContext,
waitTimeBeforeDrop: number,
waitTimeAfterDrop: number,
): Promise<number> {
const details = await fetchWebRtcAuthDetails(environment, resellerToken)
const telephony = await setupSipClient(details)
const virtualMic = audioContext.createMediaStreamDestination()
const options: CreateCallOptions = {
timeout: DEFAULT_TIMEOUT,
iceGatheringTimeout: DEFAULT_ICE_GATHERING_TIMEOUT,
mediaStream: virtualMic.stream,
}
const callApi = await telephony.createCall(destination, options)
enableMediaStreamAudioInChrome(callApi.media)
const remoteAudio = audioContext.createMediaStreamSource(callApi.media)
const virtualSpeaker = audioContext.createMediaStreamDestination()
remoteAudio.connect(virtualSpeaker)
const dialogId = getDialogId(callApi.acceptHeaders)!

await delay(waitTimeBeforeDrop)
callApi.drop()

await callApi.callCompletion

await delay(waitTimeAfterDrop)
const dialog = await fetchDialogData(environment, resellerToken, dialogId)
let synthesisCount = 0
for (let datum of dialog.data) {
if (datum.type === "Synthesis") {
synthesisCount++
}
}
return synthesisCount
}

async function performAllCalls(
environment: string,
resellerToken: string,
destination: string,
audioContext: AudioContext,
numberOfCalls: number,
maxParallelism: number,
waitTimeBeforeDrop: number,
waitTimeAfterDrop: number,
): Promise<[number, number, number, number]> {
let noGreeting: number = 0
let singleGreeting: number = 0
let multiGreeting: number = 0
let failedCalls: number = 0

const workQueue = concurrencyLimitedWorkQueue<void>(maxParallelism)

for (let i = 0; i < numberOfCalls; i++) {
workQueue.submit(async () => {
try {
const greetingCount = await performCall(environment, resellerToken, destination, audioContext, waitTimeBeforeDrop, waitTimeAfterDrop)
if (greetingCount === 0) {
noGreeting++
} else if (greetingCount === 1) {
singleGreeting++
} else {
multiGreeting++
}
} catch (e) {
failedCalls++
console.error("Call failed!", e)
}
})
}

await workQueue.awaitEmpty()
return [noGreeting, singleGreeting, multiGreeting, failedCalls]
}

window.addEventListener('DOMContentLoaded', () => {
const environment = getAndDisplayEnvironmentFromQuery()

const query = new URLSearchParams(location.search)
document.querySelectorAll<HTMLInputElement>('input[name]').forEach(element => {
const key = `form.${element.name}`
const queryValue = query.get(element.name)
const existingValue = localStorage.getItem(key)
element.addEventListener('change', () => {
localStorage.setItem(key, element.value)
updateQueryParameter(element.name, element.value)
})
if (queryValue) {
element.value = queryValue
} else if (existingValue) {
element.value = existingValue
updateQueryParameter(element.name, existingValue)
}
})

const startCallsButton = document.getElementById('start-calls')! as HTMLButtonElement

startCallsButton.addEventListener('click', e => {
e.preventDefault()
const audioContext = new AudioContext()

const resellerToken = document.querySelector<HTMLInputElement>("input#reseller-token")!!.value
const destination = document.querySelector<HTMLInputElement>("input#destination")!!.value

performAllCalls(environment, resellerToken, destination, audioContext, 6, 2, 5000, 2000)
.then(([noGreeting, singleGreeting, multiGreeting, failedCalls]) => {
alert(`Calls without greeting: ${noGreeting}\nCalls with a single greeting: ${singleGreeting}\nCalls with multiple greetings: ${multiGreeting}\nFailed calls: ${failedCalls}`)
})

})
})
9 changes: 4 additions & 5 deletions src/web-call-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import {
getAndDisplayEnvironmentFromQuery,
getCustomSipHeadersFromQuery,
getDialogId,
updateQueryParameter,
} from './common-example'

Expand Down Expand Up @@ -38,11 +39,9 @@ window.addEventListener('DOMContentLoaded', () => {
connectButton.addEventListener('call_accepted', (e) => {
const headers = e.detail.acceptHeaders
console.log("Headers received from accept:", headers)
const dialogIds = headers
.filter(([name,]) => name.toLowerCase() == "x-cvg-dialogid")
.map(([, value]) => value)
if (dialogIds.length > 0) {
console.log(`DialogId: ${dialogIds[0]}`)
const dialogId = getDialogId(headers)
if (dialogId) {
console.log(`DialogId: ${dialogId}`)
}
})

Expand Down
27 changes: 12 additions & 15 deletions src/webaudio-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function performCall(
})
}

function performAllCalls(
async function performAllCalls(
environment: string,
resellerToken: string,
destination: string,
Expand All @@ -144,22 +144,19 @@ function performAllCalls(
const failed: Array<[DecodedAudioFile, any]> = []
const workQueue = concurrencyLimitedWorkQueue<void>(maxParallelism)
for (let file of files) {
workQueue.submit(() => {
return performCall(environment, resellerToken, destination, extraCustomSipHeaders, audioContext, file)
.then(() => {
console.info(`Call completed for: ${file.toString()}`)
completed.push(file)
})
.catch(e => {
console.info(`Call failed for: ${file.toString()}`, e)
failed.push([file, e])
})
workQueue.submit(async () => {
try {
await performCall(environment, resellerToken, destination, extraCustomSipHeaders, audioContext, file)
console.info(`Call completed for: ${file.toString()}`)
completed.push(file)
} catch (e) {
console.info(`Call failed for: ${file.toString()}`, e)
failed.push([file, e])
}
})
}

return workQueue.awaitEmpty().then(() => {
return [completed, failed]
})
await workQueue.awaitEmpty()
return [completed, failed]
}

function filesDropped(files: FileList): Promise<Array<DroppedAudioFile>> {
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
entry: {
"web-call-example": "./src/web-call-example.ts",
"webaudio-example": "./src/webaudio-example.ts",
"loadtest-example": "./src/loadtest-example.ts",
"webcomponent": "./src/webcomponent.ts",
},
output: {
Expand Down

0 comments on commit 381aca2

Please sign in to comment.