From 7835ebc346c315018d7435a65bbc24760781c2d5 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:26:28 +0100 Subject: [PATCH] perf: async signal exit ftpsrv and nxlink thread in order to not block. add perf logging for exit. --- sphaira/include/ftpsrv_helper.hpp | 1 + sphaira/include/nxlink.h | 3 + sphaira/include/utils/profile.hpp | 24 +++++ sphaira/source/app.cpp | 153 ++++++++++++++++++------------ sphaira/source/download.cpp | 10 +- sphaira/source/ftpsrv_helper.cpp | 5 + sphaira/source/nxlink.cpp | 5 + 7 files changed, 134 insertions(+), 67 deletions(-) create mode 100644 sphaira/include/utils/profile.hpp diff --git a/sphaira/include/ftpsrv_helper.hpp b/sphaira/include/ftpsrv_helper.hpp index 24ef315..176e6ad 100644 --- a/sphaira/include/ftpsrv_helper.hpp +++ b/sphaira/include/ftpsrv_helper.hpp @@ -6,6 +6,7 @@ namespace sphaira::ftpsrv { bool Init(); void Exit(); +void ExitSignal(); using OnInstallStart = std::function; using OnInstallWrite = std::function; diff --git a/sphaira/include/nxlink.h b/sphaira/include/nxlink.h index da769ab..019ce44 100644 --- a/sphaira/include/nxlink.h +++ b/sphaira/include/nxlink.h @@ -44,6 +44,9 @@ bool nxlinkInitialize(NxlinkCallback callback); // signal for the event to close and then join the thread. void nxlinkExit(); +// async the exit, call this first and then call exit later to avoid blocking. +void nxlinkSignalExit(); + #ifdef __cplusplus } #endif diff --git a/sphaira/include/utils/profile.hpp b/sphaira/include/utils/profile.hpp new file mode 100644 index 0000000..c365bdd --- /dev/null +++ b/sphaira/include/utils/profile.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "ui/types.hpp" +#include "log.hpp" + +namespace sphaira::utils { + +struct ScopedTimestampProfile { + ScopedTimestampProfile(const std::string& name) : m_name{name} { + + } + + ~ScopedTimestampProfile() { + log_write("\t[%s] time taken: %.2fs %.2fms\n", m_name.c_str(), m_ts.GetSecondsD(), m_ts.GetMsD()); + } + +private: + const std::string m_name; + TimeStamp m_ts{}; +}; + +#define SCOPED_TIMESTAMP(name) sphaira::utils::ScopedTimestampProfile ANONYMOUS_VARIABLE(SCOPE_PROFILE_STATE_){name}; + +} // namespace sphaira::utils diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 1233235..0dc52d6 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -23,6 +23,7 @@ #include "web.hpp" #include "swkbd.hpp" #include "fatfs.hpp" +#include "utils/profile.hpp" #include #include @@ -1966,6 +1967,10 @@ App::~App() { appletUnhook(&m_appletHookCookie); + // async exit as these threads sleep every 100ms. + nxlinkSignalExit(); + ftpsrv::ExitSignal(); + // destroy this first as it seems to prevent a crash when exiting the appstore // when an image that was being drawn is displayed // replicate: saves -> homebrew -> misc -> appstore -> sphaira -> changelog -> exit @@ -1975,109 +1980,133 @@ App::~App() { // this has to be called before any cleanup to ensure the lifetime of // nvg is still active as some widgets may need to free images. // clear in reverse order as the widgets are a stack (todo: just use a stack?) - while (!m_widgets.empty()) { - m_widgets.pop_back(); + { + SCOPED_TIMESTAMP("widget exit"); + while (!m_widgets.empty()) { + m_widgets.pop_back(); + } } - nvgDeleteImage(vg, m_default_image); i18n::exit(); - curl::Exit(); - ini_puts("config", "theme", m_theme.meta.ini_path, CONFIG_PATH); - CloseTheme(); - - // Free any loaded sound from memory - for (auto id : m_sound_ids) { - if (id) { - plsrPlayerFree(id); - } + { + SCOPED_TIMESTAMP("curl exit"); + curl::Exit(); } - // De-initialize our player - plsrPlayerExit(); + { + SCOPED_TIMESTAMP("theme exit"); + ini_puts("config", "theme", m_theme.meta.ini_path, CONFIG_PATH); + CloseTheme(); + } - nvgDeleteDk(this->vg); - this->renderer.reset(); + // Free any loaded sound from memory + { + SCOPED_TIMESTAMP("plsr exit"); + // for (auto id : m_sound_ids) { + // if (id) { + // plsrPlayerFree(id); + // } + // } + + // De-initialize our player + plsrPlayerExit(); + } + + { + SCOPED_TIMESTAMP("nvg exit"); + nvgDeleteImage(vg, m_default_image); + nvgDeleteDk(this->vg); + this->renderer.reset(); #ifdef USE_NVJPG - m_decoder.finalize(); - nj::finalize(); + m_decoder.finalize(); + nj::finalize(); #endif + } // backup hbmenu if it is not sphaira - if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) { - NacpStruct hbmenu_nacp; - fs::FsNativeSd fs; - Result rc; + { + SCOPED_TIMESTAMP("nro copy"); + if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) { + NacpStruct hbmenu_nacp; + fs::FsNativeSd fs; + Result rc; - if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) { - log_write("backing up hbmenu.nro\n"); - if (R_FAILED(rc = fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro"))) { - log_write("failed to backup hbmenu.nro\n"); + // todo: don't read whole nacp, only the name. + // todo: keep file open and use that as part of the file copy. + if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) { + log_write("backing up hbmenu.nro\n"); + if (R_FAILED(rc = fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro"))) { + log_write("failed to backup hbmenu.nro\n"); + } + } else { + log_write("not backing up\n"); } - } else { - log_write("not backing up\n"); - } - if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) { - log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath().s, rc, R_MODULE(rc), R_DESCRIPTION(rc)); - } else { - log_write("success with copying over root file!\n"); - } - } else if (IsHbmenu()) { - // check we have a version that's newer than current. - NacpStruct hbmenu_nacp; - fs::FsNativeSd fs; - Result rc; + if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) { + log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath().s, rc, R_MODULE(rc), R_DESCRIPTION(rc)); + } else { + log_write("success with copying over root file!\n"); + } + } else if (IsHbmenu()) { + // check we have a version that's newer than current. + NacpStruct hbmenu_nacp; + fs::FsNativeSd fs; + Result rc; - // ensure that are still sphaira - if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && !std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) { - NacpStruct sphaira_nacp; - fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro"; + // ensure that are still sphaira + if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && !std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) { + NacpStruct sphaira_nacp; + fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro"; - rc = nro_get_nacp(sphaira_path, sphaira_nacp); - if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { - sphaira_path = "/switch/sphaira.nro"; rc = nro_get_nacp(sphaira_path, sphaira_nacp); - } + if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { + sphaira_path = "/switch/sphaira.nro"; + rc = nro_get_nacp(sphaira_path, sphaira_nacp); + } - // found sphaira, now lets get compare version - if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { - if (IsVersionNewer(hbmenu_nacp.display_version, sphaira_nacp.display_version)) { - if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) { - log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc)); - } else { - log_write("success with updating hbmenu!\n"); + // found sphaira, now lets get compare version + if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { + if (IsVersionNewer(hbmenu_nacp.display_version, sphaira_nacp.display_version)) { + if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) { + log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc)); + } else { + log_write("success with updating hbmenu!\n"); + } } } + } else { + log_write("no longer hbmenu!\n"); } - } else { - log_write("no longer hbmenu!\n"); } } if (App::GetMtpEnable()) { - log_write("closing mtp\n"); + SCOPED_TIMESTAMP("mtp exit"); haze::Exit(); } if (App::GetFtpEnable()) { - log_write("closing ftp\n"); + SCOPED_TIMESTAMP("ftp exit"); ftpsrv::Exit(); } if (App::GetNxlinkEnable()) { - log_write("closing nxlink\n"); + SCOPED_TIMESTAMP("nxlink exit"); nxlinkExit(); } if (App::GetHddEnable()) { - log_write("closing hdd\n"); + SCOPED_TIMESTAMP("hdd exit"); usbHsFsExit(); } - fatfs::UnmountAll(); - romfsUnmount("Qlaunch_romfs"); + { + SCOPED_TIMESTAMP("fatfs exit"); + fatfs::UnmountAll(); + romfsUnmount("Qlaunch_romfs"); + } log_write("\t[EXIT] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); diff --git a/sphaira/source/download.cpp b/sphaira/source/download.cpp index 5d81afe..6ef212f 100644 --- a/sphaira/source/download.cpp +++ b/sphaira/source/download.cpp @@ -79,8 +79,7 @@ struct Cache { using Value = std::pair; bool init() { - mutexLock(&m_mutex); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + SCOPED_MUTEX(&m_mutex); if (m_json) { return true; @@ -103,13 +102,13 @@ struct Cache { } void exit() { - mutexLock(&m_mutex); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + SCOPED_MUTEX(&m_mutex); if (!m_json) { return; } + // note: this takes 20ms if (!yyjson_mut_write_file(JSON_PATH, m_json, YYJSON_WRITE_NOFLAG, nullptr, nullptr)) { log_write("failed to write etag json: %s\n", JSON_PATH.s); } @@ -120,6 +119,8 @@ struct Cache { } void get(const fs::FsPath& path, curl::Header& header) { + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + const auto [etag, last_modified] = get_internal(path); if (!etag.empty()) { header.m_map.emplace("if-none-match", etag); @@ -131,7 +132,6 @@ struct Cache { } void set(const fs::FsPath& path, const curl::Header& value) { - mutexLock(&m_mutex); ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); std::string etag_str; diff --git a/sphaira/source/ftpsrv_helper.cpp b/sphaira/source/ftpsrv_helper.cpp index f9e021b..73f21f6 100644 --- a/sphaira/source/ftpsrv_helper.cpp +++ b/sphaira/source/ftpsrv_helper.cpp @@ -405,6 +405,11 @@ void Exit() { log_write("[FTP] exitied\n"); } +void ExitSignal() { + SCOPED_MUTEX(&g_mutex); + g_should_exit = true; +} + #if ENABLE_NETWORK_INSTALL void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close) { SCOPED_MUTEX(&g_shared_data.mutex); diff --git a/sphaira/source/nxlink.cpp b/sphaira/source/nxlink.cpp index 9ce5e1b..e3ab194 100644 --- a/sphaira/source/nxlink.cpp +++ b/sphaira/source/nxlink.cpp @@ -482,4 +482,9 @@ void nxlinkExit() { threadClose(&g_thread); } +void nxlinkSignalExit() { + std::scoped_lock lock{g_mutex}; + g_quit = true; +} + } // extern "C"