Skip to content

Commit acdebc9

Browse files
authored
Merge pull request #3460 from sniok/improve-startup-perf
app: frontend: Improve startup performance
2 parents 859f7e3 + ecd1d75 commit acdebc9

File tree

5 files changed

+226
-145
lines changed

5 files changed

+226
-145
lines changed

app/electron/main.ts

Lines changed: 82 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,9 @@ function startElecron() {
11311131
},
11321132
});
11331133

1134+
// Load the frontend
1135+
mainWindow.loadURL(startUrl);
1136+
11341137
setMenu(mainWindow, currentMenu);
11351138

11361139
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
@@ -1285,77 +1288,88 @@ function startElecron() {
12851288
new PluginManagerEventListeners().setupEventHandlers();
12861289

12871290
if (!useExternalServer) {
1288-
const runningHeadlamp = await getRunningHeadlampPIDs();
1289-
let shouldWaitForKill = true;
1290-
1291-
if (!!runningHeadlamp) {
1292-
const resp = dialog.showMessageBoxSync(mainWindow, {
1293-
// Avoiding mentioning Headlamp here because it may run under a different name depending on branding (plugins).
1294-
title: i18n.t('Another process is running'),
1295-
message: i18n.t(
1296-
'Looks like another process is already running. Continue by terminating that process automatically, or quit?'
1297-
),
1298-
type: 'question',
1299-
buttons: [i18n.t('Continue'), i18n.t('Quit')],
1300-
});
1291+
serverProcess = await startServer();
1292+
attachServerEventHandlers(serverProcess);
13011293

1302-
if (resp === 0) {
1303-
runningHeadlamp.forEach(pid => {
1304-
try {
1305-
killProcess(pid);
1306-
} catch (e) {
1307-
console.log(`Failed to quit headlamp-servere:`, e.message);
1308-
shouldWaitForKill = false;
1294+
serverProcess.addListener('exit', async e => {
1295+
const ERROR_ADDRESS_IN_USE = 98;
1296+
if (e === ERROR_ADDRESS_IN_USE) {
1297+
const runningHeadlamp = await getRunningHeadlampPIDs();
1298+
let shouldWaitForKill = true;
1299+
1300+
if (!mainWindow) {
1301+
return;
1302+
}
1303+
1304+
if (!!runningHeadlamp) {
1305+
const resp = dialog.showMessageBoxSync(mainWindow, {
1306+
// Avoiding mentioning Headlamp here because it may run under a different name depending on branding (plugins).
1307+
title: i18n.t('Another process is running'),
1308+
message: i18n.t(
1309+
'Looks like another process is already running. Continue by terminating that process automatically, or quit?'
1310+
),
1311+
type: 'question',
1312+
buttons: [i18n.t('Continue'), i18n.t('Quit')],
1313+
});
1314+
1315+
if (resp === 0) {
1316+
runningHeadlamp.forEach(pid => {
1317+
try {
1318+
killProcess(pid);
1319+
} catch (e) {
1320+
console.log(`Failed to quit headlamp-servere:`, e.message);
1321+
shouldWaitForKill = false;
1322+
}
1323+
});
1324+
} else {
1325+
mainWindow.close();
1326+
return;
13091327
}
1310-
});
1311-
} else {
1312-
mainWindow.close();
1313-
return;
1314-
}
1315-
}
1316-
1317-
// If we reach here, then we attempted to kill headlamp-server. Let's make sure it's killed
1318-
// before starting our own, or else we may end up in a race condition (failing to start the
1319-
// new one before the existing one is fully killed).
1320-
if (!!runningHeadlamp && shouldWaitForKill) {
1321-
let stillRunning = true;
1322-
let timeWaited = 0;
1323-
const maxWaitTime = 3000; // ms
1324-
// @todo: Use an iterative back-off strategy for the wait (so we can start by waiting for shorter times).
1325-
for (let tries = 1; timeWaited < maxWaitTime && stillRunning; tries++) {
1326-
console.debug(
1327-
`Checking if Headlamp is still running after we asked it to be killed; ${tries} ${timeWaited}/${maxWaitTime}ms wait.`
1328-
);
1329-
1330-
// Wait (10 * powers of 2) ms with a max of 250 ms
1331-
const waitTime = Math.min(10 * tries ** 2, 250); // ms
1332-
await new Promise(f => setTimeout(f, waitTime));
1333-
1334-
timeWaited += waitTime;
1335-
1336-
stillRunning = !!(await getRunningHeadlampPIDs());
1337-
console.debug(stillRunning ? 'Still running...' : 'No longer running!');
1328+
}
1329+
1330+
// If we reach here, then we attempted to kill headlamp-server. Let's make sure it's killed
1331+
// before starting our own, or else we may end up in a race condition (failing to start the
1332+
// new one before the existing one is fully killed).
1333+
if (!!runningHeadlamp && shouldWaitForKill) {
1334+
let stillRunning = true;
1335+
let timeWaited = 0;
1336+
const maxWaitTime = 3000; // ms
1337+
// @todo: Use an iterative back-off strategy for the wait (so we can start by waiting for shorter times).
1338+
for (let tries = 1; timeWaited < maxWaitTime && stillRunning; tries++) {
1339+
console.debug(
1340+
`Checking if Headlamp is still running after we asked it to be killed; ${tries} ${timeWaited}/${maxWaitTime}ms wait.`
1341+
);
1342+
1343+
// Wait (10 * powers of 2) ms with a max of 250 ms
1344+
const waitTime = Math.min(10 * tries ** 2, 250); // ms
1345+
await new Promise(f => setTimeout(f, waitTime));
1346+
1347+
timeWaited += waitTime;
1348+
1349+
stillRunning = !!(await getRunningHeadlampPIDs());
1350+
console.debug(stillRunning ? 'Still running...' : 'No longer running!');
1351+
}
1352+
}
1353+
1354+
// If we couldn't kill the process, warn the user and quit.
1355+
const processes = await getRunningHeadlampPIDs();
1356+
if (!!processes) {
1357+
dialog.showMessageBoxSync({
1358+
type: 'warning',
1359+
title: i18n.t('Failed to quit the other running process'),
1360+
message: i18n.t(
1361+
`Could not quit the other running process, PIDs: {{ process_list }}. Please stop that process and relaunch the app.`,
1362+
{ process_list: processes }
1363+
),
1364+
});
1365+
1366+
mainWindow.close();
1367+
return;
1368+
}
1369+
serverProcess = await startServer();
1370+
attachServerEventHandlers(serverProcess);
13381371
}
1339-
}
1340-
1341-
// If we couldn't kill the process, warn the user and quit.
1342-
const processes = await getRunningHeadlampPIDs();
1343-
if (!!processes) {
1344-
dialog.showMessageBoxSync({
1345-
type: 'warning',
1346-
title: i18n.t('Failed to quit the other running process'),
1347-
message: i18n.t(
1348-
`Could not quit the other running process, PIDs: {{ process_list }}. Please stop that process and relaunch the app.`,
1349-
{ process_list: processes }
1350-
),
1351-
});
1352-
1353-
mainWindow.close();
1354-
return;
1355-
}
1356-
1357-
serverProcess = await startServer();
1358-
attachServerEventHandlers(serverProcess);
1372+
});
13591373
}
13601374

13611375
// Also add bundled plugin bin directories to PATH
@@ -1370,9 +1384,6 @@ function startElecron() {
13701384
if (userPluginBinDirs.length > 0) {
13711385
addToPath(userPluginBinDirs, 'userPluginBinDirs plugin');
13721386
}
1373-
1374-
// Finally load the frontend
1375-
mainWindow.loadURL(startUrl);
13761387
}
13771388

13781389
if (disableGPU) {

backend/cmd/headlamp.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"regexp"
3636
"runtime"
3737
"strings"
38+
"syscall"
3839
"time"
3940

4041
oidc "github.com/coreos/go-oidc/v3/oidc"
@@ -1186,7 +1187,18 @@ func StartHeadlampServer(config *HeadlampConfig) {
11861187
// Start server
11871188
if err := http.ListenAndServe(addr, handler); err != nil { //nolint:gosec
11881189
logger.Log(logger.LevelError, nil, err, "Failed to start server")
1189-
return
1190+
1191+
HandleServerStartError(&err)
1192+
}
1193+
}
1194+
1195+
// Handle common server startup errors.
1196+
func HandleServerStartError(err *error) {
1197+
// Check if the reason server failed because the address is already in use
1198+
// this might be because backend process is already running
1199+
if errors.Is(*err, syscall.EADDRINUSE) {
1200+
// Exit with 98 (address in use) exit code
1201+
os.Exit(int(syscall.EADDRINUSE))
11901202
}
11911203
}
11921204

frontend/index.html

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,67 @@
2626
<script>
2727
headlampBaseUrl = '%BASE_URL%';
2828
</script>
29+
<style>
30+
.splash {
31+
position: absolute;
32+
top: 0;
33+
left: 0;
34+
width: 100vw;
35+
height: 100vh;
36+
display: flex;
37+
align-items: center;
38+
justify-content: center;
39+
}
40+
41+
.loader,
42+
.loader:before,
43+
.loader:after {
44+
border-radius: 50%;
45+
width: 2.5em;
46+
height: 2.5em;
47+
animation-fill-mode: both;
48+
animation: bblFadInOut 1.8s infinite ease-in-out;
49+
}
50+
.loader {
51+
font-size: 7px;
52+
position: relative;
53+
text-indent: -9999em;
54+
transform: translateZ(0);
55+
animation-delay: -0.16s;
56+
}
57+
.loader:before,
58+
.loader:after {
59+
content: '';
60+
position: absolute;
61+
top: 0;
62+
}
63+
.loader:before {
64+
left: -3.5em;
65+
animation-delay: -0.32s;
66+
}
67+
.loader:after {
68+
left: 3.5em;
69+
}
70+
71+
@keyframes bblFadInOut {
72+
0%,
73+
80%,
74+
100% {
75+
box-shadow: 0 2.5em 0 -1.3em;
76+
}
77+
40% {
78+
box-shadow: 0 2.5em 0 0;
79+
}
80+
}
81+
</style>
2982
</head>
3083
<body>
3184
<noscript>You need to enable JavaScript to run this app.</noscript>
32-
<div id="root"></div>
85+
<div id="root">
86+
<div class="splash">
87+
<span class="loader"></span>
88+
</div>
89+
</div>
3390
<!--
3491
This HTML file is a template.
3592
If you open it directly in the browser, you will see an empty page.
@@ -40,6 +97,16 @@
4097
To begin the development, run `npm start` or `yarn start`.
4198
To create a production bundle, use `npm run build` or `yarn build`.
4299
-->
100+
<script>
101+
// Set the background color to avoid flashing screen
102+
const currentThemeCacheKey = 'cached-current-theme';
103+
const cachedTheme = JSON.parse(localStorage.getItem(currentThemeCacheKey) ?? 'null');
104+
105+
if (cachedTheme) {
106+
document.body.style.backgroundColor = cachedTheme.background.default;
107+
document.body.style.color = cachedTheme.text.primary;
108+
}
109+
</script>
43110
<script type="module" src="/src/index.tsx"></script>
44111
</body>
45112
</html>

0 commit comments

Comments
 (0)