Skip to content

Commit

Permalink
fix: Set timeout for refreshing user before launch & track more detai…
Browse files Browse the repository at this point in the history
…l launch phase
  • Loading branch information
ci010 committed Dec 29, 2023
1 parent 1887872 commit e459dfd
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 96 deletions.
2 changes: 2 additions & 0 deletions xmcl-electron-app/main/definedPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { pluginOfficialUserApi } from '@xmcl/runtime/user/pluginOfficialUserApi'
import { pluginOffineUser } from '@xmcl/runtime/user/pluginOfflineUser'
import { pluginUserTokenStorage } from '@xmcl/runtime/user/pluginUserTokenStorage'
import { pluginYggdrasilHandler } from '@xmcl/runtime/yggdrasilServer/pluginYggdrasilHandler'
import { pluginLaunchPrecheck } from '@xmcl/runtime/launch/pluginLaunchPrecheck'

import { LauncherAppPlugin } from '~/app'
import { definedServices } from './definedServices'
Expand All @@ -41,6 +42,7 @@ export const definedPlugins: LauncherAppPlugin[] = [
pluginResourceWorker,
pluginEncodingWorker,
pluginSetupWorker,
pluginLaunchPrecheck,

pluginMediaProtocol,
pluginResourcePackLink,
Expand Down
66 changes: 36 additions & 30 deletions xmcl-keystone-ui/src/composables/instanceLaunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const kInstanceLaunch: InjectionKey<ReturnType<typeof useInstanceLaunch>>

export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<ResolvedVersion | { requirements: Record<string, any> } | undefined>, java: Ref<JavaRecord | undefined>, userProfile: Ref<UserProfile>, globalState: ReturnType<typeof useSettingsState>) {
const { refreshUser } = useService(UserServiceKey)
const { launch, kill, on, getGameProcesses, reportLaunchStatus } = useService(LaunchServiceKey)
const { launch, kill, on, getGameProcesses, reportOperation } = useService(LaunchServiceKey)
const { globalAssignMemory, globalMaxMemory, globalMinMemory, globalMcOptions, globalVmOptions, globalFastLaunch, globalHideLauncher, globalShowLog } = useGlobalSettings(globalState)
const { getMemoryStatus } = useService(BaseServiceKey)
const { abortRefresh } = useService(UserServiceKey)
Expand Down Expand Up @@ -55,7 +55,33 @@ export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<
data.value = data.value?.filter(p => p.pid !== pid)
})

async function generateLaunchOptions() {
async function track<T>(p: Promise<T>, name: string, id: string) {
const start = performance.now()
reportOperation({
name,
operationId: id,
})
try {
const v = await p
reportOperation({
duration: performance.now() - start,
name,
operationId: id,
success: true,
})
return v
} catch (e) {
reportOperation({
duration: performance.now() - start,
name,
operationId: id,
success: false,
})
throw e
}
}

async function generateLaunchOptions(id: string) {
const ver = resolvedVersion.value
if (!ver || 'requirements' in ver) {
throw new LaunchException({ type: 'launchNoVersionInstalled' })
Expand All @@ -71,7 +97,7 @@ export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<
if (authority && (authority.protocol === 'http:' || authority?.protocol === 'https:' || userProfile.value.authority === AUTHORITY_DEV)) {
launchingStatus.value = 'preparing-authlib'
yggdrasilAgent = {
jar: await getOrInstallAuthlibInjector(),
jar: await track(getOrInstallAuthlibInjector(), 'prepare-authlib', id),
server: userProfile.value.authority,
}
}
Expand All @@ -88,7 +114,7 @@ export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<
// noop
} else if (assignMemory === 'auto') {
launchingStatus.value = 'assigning-memory'
const mem = await getMemoryStatus()
const mem = await track(getMemoryStatus(), 'get-memory-status', id)
minMemory = Math.floor(mem.free / 1024 / 1024 - 256)
} else {
minMemory = undefined
Expand All @@ -99,6 +125,7 @@ export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<
const mcOptions = inst.mcOptions ?? globalMcOptions.value.filter(v => !!v)

const options: LaunchOptions = {
operationId: id,
version: instance.value.version || ver.id,
gameDirectory: instance.value.path,
user: userProfile.value,
Expand All @@ -118,15 +145,16 @@ export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<
async function launchGame() {
try {
error.value = undefined
const options = await generateLaunchOptions()
const operationId = crypto.getRandomValues(new Uint32Array(1))[0].toString(16)
const options = await generateLaunchOptions(operationId)

if (!options.skipAssetsCheck) {
launchingStatus.value = 'refreshing-user'
try {
await Promise.race([
new Promise((resolve) => { setTimeout(resolve, 5_000) }),
await track(Promise.race([
new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Timeout')), 5_000) }),
refreshUser(userProfile.value.id),
])
]), 'refresh-user', operationId)
} catch (e) {
}
}
Expand Down Expand Up @@ -162,28 +190,6 @@ export function useInstanceLaunch(instance: Ref<Instance>, resolvedVersion: Ref<
}
}

let last = 0
const record = {} as Record<string, number>
let timeout: any
watch(launchingStatus, (newVal, oldVal) => {
if (oldVal !== '') {
const duration = performance.now() - last
record[oldVal] = duration
record[newVal] = -1
if (!newVal) {
reportLaunchStatus(record)
clearTimeout(timeout)
}
} else {
// start timming
last = performance.now()
record[newVal] = -1
timeout = setTimeout(() => {
reportLaunchStatus(record, 30_000)
}, 30_000)
}
})

return {
launch: launchGame,
kill: killGame,
Expand Down
27 changes: 24 additions & 3 deletions xmcl-runtime-api/src/services/LaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ interface LaunchServiceEventMap {
'minecraft-exit': LaunchOptions & { pid: number; code?: number; signal?: string; duration: number; crashReport?: string; crashReportLocation?: string; errorLog: string }
'minecraft-stdout': { pid: number; stdout: string }
'minecraft-stderr': { pid: number; stdout: string }
'minecraft-launch-status-pre': { record: Record<string, number>; alreadyTimeout?: string }
'launch-performance-pre': { id: string; name: string }
'launch-performance': { id: string; name: string; duration: number }
'error': LaunchException
}

export interface LaunchOptions {
/**
* The operation id for telemery
*/
operationId?: string
/**
* Override selected version for current instance
*/
Expand Down Expand Up @@ -108,6 +113,20 @@ export interface GameProcess {
options: LaunchOptions
}

export interface ReportOperationPayload {
operationId: string
/**
* Name of the operation
*/
name: string
/**
* The duration of the operation. If empty, it means the operation is just started
*/
duration?: number

success?: boolean
}

export interface LaunchService extends GenericEventEmitter<LaunchServiceEventMap> {
/**
* Generate useable launch arguments for current profile
Expand All @@ -132,8 +151,10 @@ export interface LaunchService extends GenericEventEmitter<LaunchServiceEventMap
* Get all game processes
*/
getGameProcesses(): Promise<GameProcess[]>

reportLaunchStatus(record: Record<string, number>, alreadyTimeout?: number): Promise<void>
/**
* Only used for telemetry
*/
reportOperation(options: ReportOperationPayload): Promise<void>
}

export type LaunchExceptions = {
Expand Down
21 changes: 11 additions & 10 deletions xmcl-runtime/launch/LaunchMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { LaunchOption as ResolvedLaunchOptions } from '@xmcl/core'
import { LaunchOption as ResolvedLaunchOptions, ResolvedVersion } from '@xmcl/core'
import { LaunchOptions } from '@xmcl/runtime-api'

export interface LaunchMiddleware {
onBeforeLaunch(input: LaunchOptions, output: ResolvedLaunchOptions, context: Record<string, any>): Promise<void>
name: string
onBeforeLaunch(input: LaunchOptions, output: ResolvedLaunchOptions & { version: ResolvedVersion }, context: Record<string, any>): Promise<void>
onAfterLaunch?(result: {
/**
* The code of the process exit. This is the nodejs child process "exit" event arg.
*/
* The code of the process exit. This is the nodejs child process "exit" event arg.
*/
code: number
/**
* The signal of the process exit. This is the nodejs child process "exit" event arg.
*/
* The signal of the process exit. This is the nodejs child process "exit" event arg.
*/
signal: string
/**
* The crash report content
*/
* The crash report content
*/
crashReport: string
/**
* The location of the crash report
*/
* The location of the crash report
*/
crashReportLocation: string
}, output: ResolvedLaunchOptions, context: Record<string, any>): void
}
Loading

0 comments on commit e459dfd

Please sign in to comment.