Stream handling / Stream idle mode v2; a ton of network related changes (ping; scene indicator -> stream indicator, stream resizing (even on pause)) (#5312)
* Add back stream idle mode
* Shut up codespell
* Correct serialization; only expose at user level
* cargo fmt
* tsc lint fmt
* Move engineStreamMachine as a global actor; tons of more work
* Fix up everything after bumping kittycad/lib
* Remove camera sync
* Use pause/play iconology
* Add back better ping indicator
* wip
* Fix streamIdleMode checkbox being wonky
* yarn fmt
* Massive extinction event for waitForExecutionDone; try to stop projects view switching from crashing
* Clear diagnostics when unmounting code editor!
* wip
* Rework initial root projects dir + deflake many projects tests
* More e2e fixes
* Deflake revolve some revolve tests
* Fix the rest of the mfing tests
* yarn fmt
* yarn lint
* yarn tsc
* Fix tsc after rebase
* wip
* less flaky point and click
* wip
* Fixup after rebase
* Fix more tests
* Fix 2 more
* Fix up named-views tests
* yarn fmt lint tsc
* Fix up new changes
* Get rid of 1 cyclic dependency
* Fix another cyclic mfer!
* fmt
* fmt tsc
* Fix zoom to fit being frigged
* a new list of circular deps
* Remove NetworkHealthIndicator test that was shit
* Fix the bad reload repeat issue kevin started on
* Fix zoom to fit at the right moments...
* Fix cache count numbers in editor test
* Remove a test race - poll window info.
* Qualify fail function
* Try something
* Use scene.connectionEstablished
* Hopefully fix snapshots at least
* Add app console.log
* Fix native menu tests more
* tsc lint
* Fix camera failure
* Try again
* Test attempt number 15345203, action!
* Add back old window detection heuristic
* Remove firstWindow to complete the work of 2342d04fe2
* Tweak some tests for MacOS
* Tweak "set appearance" test for MacOS
Revert this if it messes up any other platform's color checks!
* Are you serious? This was all that needed formatting?
* More color tweaks
Local MacOS and CI MacOS don't agree
* Fixes on apperance e2e test for stream idle branch (#6168)
pierremtb/stream-idle-revamp-appearance-fixes
* Another apperance fix
* Skip one native menu test to make stream idle green (#6169)
* pierremtb/stream-idle-revamp-more-fixes
* Fix lint
* Update snapshot for test_generate_settings_docs
---------
Co-authored-by: lee-at-zoo-corp <lee@zoo.dev>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
This commit is contained in:
@ -20,8 +20,7 @@ import {
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { binaryToUuid, uuidv4 } from '@src/lib/utils'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 5_000
|
||||
const pingIntervalMs = 1_000
|
||||
|
||||
function isHighlightSetEntity_type(
|
||||
data: any
|
||||
@ -191,8 +190,6 @@ export type EngineConnectionState =
|
||||
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
|
||||
| State<EngineConnectionStateType.Disconnected, void>
|
||||
|
||||
export type PingPongState = 'OK' | 'TIMEOUT'
|
||||
|
||||
export enum EngineConnectionEvents {
|
||||
// Fires for each ping-pong success or failure.
|
||||
PingPongChanged = 'ping-pong-changed', // (state: PingPongState) => void
|
||||
@ -301,13 +298,18 @@ class EngineConnection extends EventTarget {
|
||||
public webrtcStatsCollector?: () => Promise<ClientMetrics>
|
||||
private engineCommandManager: EngineCommandManager
|
||||
|
||||
private pingPongSpan: { ping?: Date; pong?: Date }
|
||||
private pingPongSpan: { ping?: number; pong?: number }
|
||||
private pingIntervalId: ReturnType<typeof setInterval> = setInterval(
|
||||
() => {},
|
||||
60_000
|
||||
)
|
||||
isUsingConnectionLite: boolean = false
|
||||
|
||||
timeoutToForceConnectId: ReturnType<typeof setTimeout> = setTimeout(
|
||||
() => {},
|
||||
3000
|
||||
)
|
||||
|
||||
constructor({
|
||||
engineCommandManager,
|
||||
url,
|
||||
@ -333,74 +335,26 @@ class EngineConnection extends EventTarget {
|
||||
return
|
||||
}
|
||||
|
||||
// Without an interval ping, our connection will timeout.
|
||||
// If this.idleMode is true we skip this logic so only reconnect
|
||||
// happens on mouse move
|
||||
this.pingIntervalId = setInterval(() => {
|
||||
if (this.idleMode) return
|
||||
// Only start a new ping when the other is fulfilled.
|
||||
if (this.pingPongSpan.ping) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (this.state.type as EngineConnectionStateType) {
|
||||
case EngineConnectionStateType.ConnectionEstablished:
|
||||
// If there was no reply to the last ping, report a timeout and
|
||||
// teardown the connection.
|
||||
if (this.pingPongSpan.ping && !this.pingPongSpan.pong) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||
detail: 'TIMEOUT',
|
||||
})
|
||||
)
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectingType.Timeout,
|
||||
},
|
||||
}
|
||||
this.disconnectAll()
|
||||
// Don't start pinging until we're connected.
|
||||
if (this.state.type !== EngineConnectionStateType.ConnectionEstablished) {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise check the time between was >= pingIntervalMs,
|
||||
// and if it was, then it's bad network health.
|
||||
} else if (this.pingPongSpan.ping && this.pingPongSpan.pong) {
|
||||
if (
|
||||
Math.abs(
|
||||
this.pingPongSpan.pong.valueOf() -
|
||||
this.pingPongSpan.ping.valueOf()
|
||||
) >= pingIntervalMs
|
||||
) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||
detail: 'TIMEOUT',
|
||||
})
|
||||
)
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||
detail: 'OK',
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.send({ type: 'ping' })
|
||||
this.pingPongSpan.ping = new Date()
|
||||
this.pingPongSpan.pong = undefined
|
||||
break
|
||||
case EngineConnectionStateType.Disconnecting:
|
||||
case EngineConnectionStateType.Disconnected:
|
||||
// We will do reconnection elsewhere, because we basically need
|
||||
// to destroy this EngineConnection, and this setInterval loop
|
||||
// lives inside it. (lee) I might change this in the future so it's
|
||||
// outside this class.
|
||||
break
|
||||
default:
|
||||
if (this.isConnecting()) break
|
||||
// Means we never could do an initial connection. Reconnect everything.
|
||||
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
|
||||
break
|
||||
this.send({ type: 'ping' })
|
||||
this.pingPongSpan = {
|
||||
ping: Date.now(),
|
||||
pong: undefined,
|
||||
}
|
||||
}, pingIntervalMs)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.connect()
|
||||
this.connect({ reconnect: false })
|
||||
}
|
||||
|
||||
// SHOULD ONLY BE USED FOR VITESTS
|
||||
@ -511,7 +465,9 @@ class EngineConnection extends EventTarget {
|
||||
this.idleMode = opts?.idleMode ?? false
|
||||
clearInterval(this.pingIntervalId)
|
||||
|
||||
if (opts?.idleMode) {
|
||||
this.disconnectAll()
|
||||
|
||||
if (this.idleMode) {
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
@ -530,8 +486,6 @@ class EngineConnection extends EventTarget {
|
||||
type: DisconnectingType.Quit,
|
||||
},
|
||||
}
|
||||
|
||||
this.disconnectAll()
|
||||
}
|
||||
|
||||
initiateConnectionExclusive(): boolean {
|
||||
@ -585,8 +539,8 @@ class EngineConnection extends EventTarget {
|
||||
* This will attempt the full handshake, and retry if the connection
|
||||
* did not establish.
|
||||
*/
|
||||
connect(reconnecting?: boolean): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
connect(args: { reconnect: boolean }): Promise<void> {
|
||||
// eslint-disable-next-line
|
||||
const that = this
|
||||
return new Promise((resolve) => {
|
||||
if (this.isConnecting() || this.isReady()) {
|
||||
@ -647,7 +601,7 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
// Sometimes the remote end doesn't report the end of candidates.
|
||||
// They have 3 seconds to.
|
||||
setTimeout(() => {
|
||||
this.timeoutToForceConnectId = setTimeout(() => {
|
||||
if (that.initiateConnectionExclusive()) {
|
||||
console.warn('connected after 3 second delay')
|
||||
}
|
||||
@ -958,7 +912,7 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
// Send an initial ping
|
||||
this.send({ type: 'ping' })
|
||||
this.pingPongSpan.ping = new Date()
|
||||
this.pingPongSpan.ping = Date.now()
|
||||
}
|
||||
this.websocket.addEventListener('open', this.onWebSocketOpen)
|
||||
|
||||
@ -1053,7 +1007,20 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
switch (resp.type) {
|
||||
case 'pong':
|
||||
this.pingPongSpan.pong = new Date()
|
||||
this.pingPongSpan.pong = Date.now()
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||
detail: Math.min(
|
||||
999,
|
||||
Math.floor(
|
||||
this.pingPongSpan.pong - (this.pingPongSpan.ping ?? 0)
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
// Clear the initial ping so our interval ping loop can fire again
|
||||
// But only after using it above!
|
||||
this.pingPongSpan.ping = undefined
|
||||
break
|
||||
|
||||
case 'modeling_session_data':
|
||||
@ -1197,7 +1164,7 @@ class EngineConnection extends EventTarget {
|
||||
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
||||
}
|
||||
|
||||
if (reconnecting) {
|
||||
if (args.reconnect) {
|
||||
createWebSocketConnection()
|
||||
} else {
|
||||
this.onNetworkStatusReady = () => {
|
||||
@ -1210,9 +1177,12 @@ class EngineConnection extends EventTarget {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Do not change this back to an object or any, we should only be sending the
|
||||
// WebSocketRequest type!
|
||||
unreliableSend(message: Models['WebSocketRequest_type']) {
|
||||
if (this.unreliableDataChannel?.readyState !== 'open') return
|
||||
|
||||
// TODO(paultag): Add in logic to determine the connection state and
|
||||
// take actions if needed?
|
||||
this.unreliableDataChannel?.send(
|
||||
@ -1223,7 +1193,7 @@ class EngineConnection extends EventTarget {
|
||||
// WebSocketRequest type!
|
||||
send(message: Models['WebSocketRequest_type']) {
|
||||
// Not connected, don't send anything
|
||||
if (this.websocket?.readyState === 3) return
|
||||
if (this.websocket?.readyState !== 1) return
|
||||
|
||||
// TODO(paultag): Add in logic to determine the connection state and
|
||||
// take actions if needed?
|
||||
@ -1232,6 +1202,8 @@ class EngineConnection extends EventTarget {
|
||||
)
|
||||
}
|
||||
disconnectAll() {
|
||||
clearTimeout(this.timeoutToForceConnectId)
|
||||
|
||||
if (this.websocket?.readyState === 1) {
|
||||
this.websocket?.close()
|
||||
}
|
||||
@ -1261,8 +1233,17 @@ class EngineConnection extends EventTarget {
|
||||
this.websocket?.readyState === 3
|
||||
|
||||
if (closedPc && closedUDC && closedWS) {
|
||||
// Do not notify the rest of the program that we have cut off anything.
|
||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||
if (!this.idleMode) {
|
||||
// Do not notify the rest of the program that we have cut off anything.
|
||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||
} else {
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectingType.Pause,
|
||||
},
|
||||
}
|
||||
}
|
||||
this.triggeredStart = false
|
||||
}
|
||||
}
|
||||
@ -1288,23 +1269,32 @@ export interface Subscription<T extends ModelTypes> {
|
||||
) => void
|
||||
}
|
||||
|
||||
export enum CommandLogType {
|
||||
SendModeling = 'send-modeling',
|
||||
SendScene = 'send-scene',
|
||||
ReceiveReliable = 'receive-reliable',
|
||||
ExecutionDone = 'execution-done',
|
||||
ExportDone = 'export-done',
|
||||
SetDefaultSystemProperties = 'set_default_system_properties',
|
||||
}
|
||||
|
||||
export type CommandLog =
|
||||
| {
|
||||
type: 'send-modeling'
|
||||
type: CommandLogType.SendModeling
|
||||
data: EngineCommand
|
||||
}
|
||||
| {
|
||||
type: 'send-scene'
|
||||
type: CommandLogType.SendScene
|
||||
data: EngineCommand
|
||||
}
|
||||
| {
|
||||
type: 'receive-reliable'
|
||||
type: CommandLogType.ReceiveReliable
|
||||
data: OkWebSocketResponseData
|
||||
id: string
|
||||
cmd_type?: string
|
||||
}
|
||||
| {
|
||||
type: 'execution-done'
|
||||
type: CommandLogType.ExecutionDone
|
||||
data: null
|
||||
}
|
||||
|
||||
@ -1371,8 +1361,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
height: 1337,
|
||||
}
|
||||
|
||||
elVideo: HTMLVideoElement | null = null
|
||||
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
|
||||
subscriptions: {
|
||||
@ -1525,6 +1513,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
})
|
||||
|
||||
this._camControlsCameraChange()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.sendSceneCommand({
|
||||
// CameraControls subscribes to default_camera_get_settings response events
|
||||
@ -1535,6 +1524,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
|
||||
setIsStreamReady(true)
|
||||
|
||||
// Other parts of the application should use this to react on scene ready.
|
||||
@ -1646,7 +1636,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
message.request_id
|
||||
) {
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
type: CommandLogType.ReceiveReliable,
|
||||
data: message.resp,
|
||||
id: message?.request_id || '',
|
||||
cmd_type: pending?.command?.cmd?.type,
|
||||
@ -1680,7 +1670,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
if (!command) return
|
||||
if (command.type === 'modeling_cmd_req')
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
type: CommandLogType.ReceiveReliable,
|
||||
data: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
@ -1722,7 +1712,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineConnection?.connect()
|
||||
this.engineConnection?.connect({ reconnect: false })
|
||||
}
|
||||
this.engineConnection.addEventListener(
|
||||
EngineConnectionEvents.ConnectionStarted,
|
||||
@ -1748,7 +1738,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
cmd: {
|
||||
type: 'reconfigure_stream',
|
||||
...this.streamDimensions,
|
||||
fps: 60,
|
||||
fps: 60, // This is required but it does next to nothing
|
||||
},
|
||||
}
|
||||
this.engineConnection?.send(resizeCmd)
|
||||
@ -1789,7 +1779,10 @@ export class EngineCommandManager extends EventTarget {
|
||||
} else if (this.engineCommandManager?.engineConnection) {
|
||||
// @ts-ignore
|
||||
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
||||
// @ts-ignore
|
||||
this.engineCommandManager.engineConnection = null
|
||||
}
|
||||
this.engineConnection = undefined
|
||||
}
|
||||
async startNewSession() {
|
||||
this.responseMap = {}
|
||||
@ -1864,7 +1857,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
) {
|
||||
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
||||
this.addCommandLog({
|
||||
type: 'send-scene',
|
||||
type: CommandLogType.SendScene,
|
||||
data: command,
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user