Step 200: startup and performance improvements
This commit is contained in:
@@ -547,4 +547,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
||||
| 2026-02-10 | Codex | Step 197: Keyboard navigation audit (F6 panel cycle, Esc focus return, focus ring defaults, nav focus enabled on panels). 1/1 tests pass (step197_test). |
|
||||
| 2026-02-10 | Codex | Step 198: Virtual scrolling for large files (viewport + buffered rendering, fold hiding optimized). 1/1 tests pass (step198_test). |
|
||||
| 2026-02-10 | Codex | Step 199: Large file handling (size thresholds, text-mode fallback, large file prompt, highlight disable, memory indicator). 1/1 tests pass (step199_test). |
|
||||
| 2026-02-10 | Codex | Step 200: Startup/perf (LSP & highlight debounce, deferred AST sync on session restore, debug frame budget warnings). 1/1 tests pass (step200_test). |
|
||||
| 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. |
|
||||
|
||||
@@ -1136,6 +1136,9 @@ target_include_directories(step198_test PRIVATE src)
|
||||
|
||||
add_executable(step199_test tests/step199_test.cpp)
|
||||
target_include_directories(step199_test PRIVATE src)
|
||||
|
||||
add_executable(step200_test tests/step200_test.cpp)
|
||||
target_include_directories(step200_test PRIVATE src)
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -158,6 +158,9 @@ struct BufferState {
|
||||
size_t fileSizeBytes = 0;
|
||||
bool largeFileMode = false;
|
||||
bool disableSyntaxHighlight = false;
|
||||
double highlightRequestTime = 0.0;
|
||||
bool pendingAstSync = false;
|
||||
double pendingAstStart = 0.0;
|
||||
};
|
||||
|
||||
struct EditorState {
|
||||
@@ -205,6 +208,9 @@ struct EditorState {
|
||||
bool completionVisible = false;
|
||||
int completionSelected = 0;
|
||||
double completionLastChange = 0.0;
|
||||
bool lspChangePending = false;
|
||||
double lspChangeLast = 0.0;
|
||||
std::string lspChangePath;
|
||||
bool hoverPending = false;
|
||||
double hoverLastMove = 0.0;
|
||||
ImVec2 hoverLastMouse = ImVec2(-1.0f, -1.0f);
|
||||
@@ -213,6 +219,7 @@ struct EditorState {
|
||||
int definitionRequestLine = 0;
|
||||
int definitionRequestCol = 0;
|
||||
std::string definitionRequestPath;
|
||||
double lastFrameBudgetWarning = 0.0;
|
||||
LSPClient::DefinitionLocation definitionPreview;
|
||||
bool definitionPreviewValid = false;
|
||||
Pipeline pipeline;
|
||||
@@ -592,7 +599,8 @@ struct EditorState {
|
||||
BufferManager::BufferMode mode = BufferManager::BufferMode::Structured,
|
||||
size_t fileSizeBytes = 0,
|
||||
bool largeFileMode = false,
|
||||
bool disableSyntaxHighlight = false) {
|
||||
bool disableSyntaxHighlight = false,
|
||||
bool deferAstSync = false) {
|
||||
BufferManager::BufferMode effectiveMode = mode;
|
||||
if (language == "org") effectiveMode = BufferManager::BufferMode::Text;
|
||||
if (buffers.hasBuffer(path)) {
|
||||
@@ -611,9 +619,14 @@ struct EditorState {
|
||||
state->generatedMode.setLanguage(language);
|
||||
state->editor.setContent(content, language);
|
||||
if (effectiveMode == BufferManager::BufferMode::Structured) {
|
||||
state->sync.setText(content, language);
|
||||
state->sync.syncNow();
|
||||
state->incrementalOptimizer.setRoot(state->sync.getAST());
|
||||
if (deferAstSync) {
|
||||
state->pendingAstSync = true;
|
||||
state->pendingAstStart = ImGui::GetTime();
|
||||
} else {
|
||||
state->sync.setText(content, language);
|
||||
state->sync.syncNow();
|
||||
state->incrementalOptimizer.setRoot(state->sync.getAST());
|
||||
}
|
||||
}
|
||||
state->editBuf = content;
|
||||
state->highlightsDirty = true;
|
||||
@@ -924,7 +937,7 @@ struct EditorState {
|
||||
}
|
||||
ui.layoutPreset = LayoutManager::presetFromName(session.layoutPreset);
|
||||
for (const auto& buf : session.buffers) {
|
||||
doOpen(buf.path, bufferModeFromString(buf.mode));
|
||||
doOpen(buf.path, bufferModeFromString(buf.mode), true);
|
||||
auto it = bufferStates.find(buf.path);
|
||||
if (it != bufferStates.end()) {
|
||||
auto* bs = it->second.get();
|
||||
@@ -960,6 +973,44 @@ struct EditorState {
|
||||
}
|
||||
}
|
||||
|
||||
void queueLspDidChange() {
|
||||
if (!lsp || !active()) return;
|
||||
if (active()->path.rfind("(untitled", 0) == 0) return;
|
||||
active()->lspVersion += 1;
|
||||
lspChangePending = true;
|
||||
lspChangeLast = ImGui::GetTime();
|
||||
lspChangePath = active()->path;
|
||||
}
|
||||
|
||||
void flushLspDidChange(double nowSeconds) {
|
||||
if (!lsp || !lspChangePending) return;
|
||||
double delay = settings.getLspDebounceMs() / 1000.0;
|
||||
if ((nowSeconds - lspChangeLast) < delay) return;
|
||||
auto it = bufferStates.find(lspChangePath);
|
||||
if (it == bufferStates.end()) {
|
||||
lspChangePending = false;
|
||||
return;
|
||||
}
|
||||
auto* buf = it->second.get();
|
||||
lsp->didChange(toFileUri(buf->path), buf->editBuf, buf->lspVersion);
|
||||
lspChangePending = false;
|
||||
}
|
||||
|
||||
void flushDeferredAstSync(double nowSeconds) {
|
||||
(void)nowSeconds;
|
||||
for (auto& kv : bufferStates) {
|
||||
auto* buf = kv.second.get();
|
||||
if (!buf->pendingAstSync) continue;
|
||||
buf->sync.setText(buf->editBuf, buf->language);
|
||||
buf->sync.syncNow();
|
||||
buf->incrementalOptimizer.setRoot(buf->sync.getAST());
|
||||
buf->pendingAstSync = false;
|
||||
buf->highlightsDirty = true;
|
||||
buf->generatedHighlightsDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasSidePanels() const {
|
||||
if (ui.showOutline) return true;
|
||||
if (library.showDependencyPanel) return true;
|
||||
@@ -1791,12 +1842,10 @@ struct EditorState {
|
||||
}
|
||||
active()->orchestratorDirty = false;
|
||||
active()->highlightsDirty = true;
|
||||
active()->highlightRequestTime = ImGui::GetTime();
|
||||
active()->generatedHighlightsDirty = true;
|
||||
active()->modified = true;
|
||||
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
|
||||
active()->lspVersion += 1;
|
||||
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
|
||||
}
|
||||
queueLspDidChange();
|
||||
}
|
||||
|
||||
void registerCommand(const std::string& id,
|
||||
@@ -2176,12 +2225,10 @@ struct EditorState {
|
||||
}
|
||||
active()->orchestratorDirty = true;
|
||||
active()->highlightsDirty = true;
|
||||
active()->highlightRequestTime = ImGui::GetTime();
|
||||
active()->generatedHighlightsDirty = true;
|
||||
active()->modified = true;
|
||||
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
|
||||
active()->lspVersion += 1;
|
||||
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
|
||||
}
|
||||
queueLspDidChange();
|
||||
events.publish(UIEventType::FileModified, active()->path, {}, ImGui::GetTime());
|
||||
events.publishDebounced(UIEventType::ASTChanged,
|
||||
active()->path,
|
||||
@@ -2408,7 +2455,8 @@ struct EditorState {
|
||||
}
|
||||
|
||||
void doOpen(const std::string& path,
|
||||
BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured) {
|
||||
BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured,
|
||||
bool deferAstSync = false) {
|
||||
std::error_code sizeErr;
|
||||
size_t fileSizeBytes = 0;
|
||||
if (!path.empty()) {
|
||||
@@ -2455,7 +2503,7 @@ struct EditorState {
|
||||
else if (path.size() > 4 && path.substr(path.size() - 4) == ".org")
|
||||
language = "org";
|
||||
createBuffer(path, content, language, effectiveMode,
|
||||
fileSizeBytes, largeFileMode, disableHighlight);
|
||||
fileSizeBytes, largeFileMode, disableHighlight, deferAstSync);
|
||||
welcome.addRecentFile(path, language, bufferModeToString(effectiveMode));
|
||||
saveRecentFiles();
|
||||
if (autoText && modeOverride == BufferManager::BufferMode::Structured) {
|
||||
@@ -2489,6 +2537,7 @@ struct EditorState {
|
||||
buf->incrementalOptimizer.setRoot(buf->sync.getAST());
|
||||
}
|
||||
buf->highlightsDirty = true;
|
||||
buf->highlightRequestTime = ImGui::GetTime();
|
||||
buf->generatedHighlightsDirty = true;
|
||||
buf->modified = false;
|
||||
notify(NotificationLevel::Info, "Reloaded: " + path);
|
||||
@@ -2511,6 +2560,12 @@ struct EditorState {
|
||||
void updateHighlights() {
|
||||
if (!active()) return;
|
||||
if (!active()->highlightsDirty) return;
|
||||
double now = ImGui::GetTime();
|
||||
double delay = settings.getHighlightDebounceMs() / 1000.0;
|
||||
if (active()->highlightRequestTime > 0.0 &&
|
||||
(now - active()->highlightRequestTime) < delay) {
|
||||
return;
|
||||
}
|
||||
if (active()->disableSyntaxHighlight) {
|
||||
active()->highlights.clear();
|
||||
active()->highlightsDirty = false;
|
||||
@@ -2599,11 +2654,9 @@ struct EditorState {
|
||||
active()->editBuf = active()->sync.getText();
|
||||
active()->editor.setContent(active()->editBuf, active()->language);
|
||||
active()->highlightsDirty = true;
|
||||
active()->highlightRequestTime = ImGui::GetTime();
|
||||
active()->modified = true;
|
||||
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
|
||||
active()->lspVersion += 1;
|
||||
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
|
||||
}
|
||||
queueLspDidChange();
|
||||
analysisPending = true;
|
||||
analysisLastChange = ImGui::GetTime();
|
||||
}
|
||||
|
||||
@@ -68,6 +68,12 @@ public:
|
||||
void setLargeFileTextMB(int value) { largeFileTextMB_ = value; }
|
||||
int getLargeFileDisableHighlightMB() const { return largeFileDisableHighlightMB_; }
|
||||
void setLargeFileDisableHighlightMB(int value) { largeFileDisableHighlightMB_ = value; }
|
||||
int getLspDebounceMs() const { return lspDebounceMs_; }
|
||||
void setLspDebounceMs(int value) { lspDebounceMs_ = value; }
|
||||
int getDiagnosticsDebounceMs() const { return diagnosticsDebounceMs_; }
|
||||
void setDiagnosticsDebounceMs(int value) { diagnosticsDebounceMs_ = value; }
|
||||
int getHighlightDebounceMs() const { return highlightDebounceMs_; }
|
||||
void setHighlightDebounceMs(int value) { highlightDebounceMs_ = value; }
|
||||
const std::string& getLayoutPreset() const { return layoutPreset_; }
|
||||
void setLayoutPreset(const std::string& name) { layoutPreset_ = name; }
|
||||
const std::string& getKeybindingProfile() const { return keybindingProfile_; }
|
||||
@@ -98,6 +104,9 @@ public:
|
||||
largeFileWarnMB_ = j.value("largeFileWarnMB", largeFileWarnMB_);
|
||||
largeFileTextMB_ = j.value("largeFileTextMB", largeFileTextMB_);
|
||||
largeFileDisableHighlightMB_ = j.value("largeFileDisableHighlightMB", largeFileDisableHighlightMB_);
|
||||
lspDebounceMs_ = j.value("lspDebounceMs", lspDebounceMs_);
|
||||
diagnosticsDebounceMs_ = j.value("diagnosticsDebounceMs", diagnosticsDebounceMs_);
|
||||
highlightDebounceMs_ = j.value("highlightDebounceMs", highlightDebounceMs_);
|
||||
layoutPreset_ = j.value("layoutPreset", layoutPreset_);
|
||||
keybindingProfile_ = j.value("keybindingProfile", keybindingProfile_);
|
||||
emacsConfigPath_ = j.value("emacsConfigPath", emacsConfigPath_);
|
||||
@@ -141,6 +150,9 @@ public:
|
||||
j["largeFileWarnMB"] = largeFileWarnMB_;
|
||||
j["largeFileTextMB"] = largeFileTextMB_;
|
||||
j["largeFileDisableHighlightMB"] = largeFileDisableHighlightMB_;
|
||||
j["lspDebounceMs"] = lspDebounceMs_;
|
||||
j["diagnosticsDebounceMs"] = diagnosticsDebounceMs_;
|
||||
j["highlightDebounceMs"] = highlightDebounceMs_;
|
||||
j["layoutPreset"] = layoutPreset_;
|
||||
j["keybindingProfile"] = keybindingProfile_;
|
||||
j["emacsConfigPath"] = emacsConfigPath_;
|
||||
@@ -221,6 +233,9 @@ private:
|
||||
int largeFileWarnMB_ = 1;
|
||||
int largeFileTextMB_ = 5;
|
||||
int largeFileDisableHighlightMB_ = 10;
|
||||
int lspDebounceMs_ = 120;
|
||||
int diagnosticsDebounceMs_ = 500;
|
||||
int highlightDebounceMs_ = 120;
|
||||
std::string layoutPreset_ = "VSCode";
|
||||
std::string keybindingProfile_ = "VSCode";
|
||||
|
||||
|
||||
@@ -330,12 +330,22 @@ int main(int, char**) {
|
||||
}
|
||||
}
|
||||
state.pollLspMessages();
|
||||
state.flushLspDidChange(ImGui::GetTime());
|
||||
state.flushDeferredAstSync(ImGui::GetTime());
|
||||
state.processLibraryIndexResponses();
|
||||
if (state.emacsState.emacsFunctionIndexDirty) {
|
||||
state.updateEmacsFunctionIndex();
|
||||
}
|
||||
state.refreshEmacsModeLine(ImGui::GetTime());
|
||||
state.events.tick(ImGui::GetTime());
|
||||
#ifdef _DEBUG
|
||||
double now = ImGui::GetTime();
|
||||
if (io.DeltaTime > 0.016 && (now - state.lastFrameBudgetWarning) > 1.0) {
|
||||
state.notify(NotificationLevel::Warning,
|
||||
"[perf] Frame time exceeded 16ms budget.");
|
||||
state.lastFrameBudgetWarning = now;
|
||||
}
|
||||
#endif
|
||||
themes.refreshWatchedThemes();
|
||||
if (state.fontsDirty) {
|
||||
reloadFonts();
|
||||
|
||||
@@ -372,7 +372,8 @@ static void renderEditorPanel(EditorState& state) {
|
||||
state.symbolsPending = false;
|
||||
}
|
||||
|
||||
if (state.analysisPending && (now - state.analysisLastChange) > 0.5) {
|
||||
double diagDelay = state.settings.getDiagnosticsDebounceMs() / 1000.0;
|
||||
if (state.analysisPending && (now - state.analysisLastChange) > diagDelay) {
|
||||
if (state.isStructured()) {
|
||||
auto result = state.pipeline.run(state.active()->editBuf,
|
||||
state.active()->language,
|
||||
|
||||
@@ -381,6 +381,27 @@ static void renderSettingsPanel(EditorState& state) {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Debounce (ms)");
|
||||
int lspDebounce = state.settings.getLspDebounceMs();
|
||||
if (ImGui::InputInt("LSP Change", &lspDebounce)) {
|
||||
lspDebounce = std::max(0, lspDebounce);
|
||||
state.settings.setLspDebounceMs(lspDebounce);
|
||||
settingsChanged = true;
|
||||
}
|
||||
int diagDebounce = state.settings.getDiagnosticsDebounceMs();
|
||||
if (ImGui::InputInt("Diagnostics", &diagDebounce)) {
|
||||
diagDebounce = std::max(0, diagDebounce);
|
||||
state.settings.setDiagnosticsDebounceMs(diagDebounce);
|
||||
settingsChanged = true;
|
||||
}
|
||||
int highlightDebounce = state.settings.getHighlightDebounceMs();
|
||||
if (ImGui::InputInt("Highlighting", &highlightDebounce)) {
|
||||
highlightDebounce = std::max(0, highlightDebounce);
|
||||
state.settings.setHighlightDebounceMs(highlightDebounce);
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
LayoutPreset preset = state.ui.layoutPreset;
|
||||
int presetIndex = 0;
|
||||
if (preset == LayoutPreset::Emacs) presetIndex = 1;
|
||||
|
||||
37
editor/tests/step200_test.cpp
Normal file
37
editor/tests/step200_test.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
// Step 200: Startup and panel performance.
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static void assertContains(const std::string& text, const std::string& needle) {
|
||||
assert(text.find(needle) != std::string::npos);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const std::string settings = readFile("src/SettingsManager.h");
|
||||
const std::string editorState = readFile("src/EditorState.h");
|
||||
const std::string mainSrc = readFile("src/main.cpp");
|
||||
const std::string editorPanel = readFile("src/panels/EditorPanel.h");
|
||||
|
||||
assertContains(settings, "getLspDebounceMs");
|
||||
assertContains(settings, "getDiagnosticsDebounceMs");
|
||||
assertContains(settings, "getHighlightDebounceMs");
|
||||
|
||||
assertContains(editorState, "flushDeferredAstSync");
|
||||
assertContains(editorState, "flushLspDidChange");
|
||||
assertContains(editorState, "pendingAstSync");
|
||||
|
||||
assertContains(editorPanel, "getDiagnosticsDebounceMs");
|
||||
assertContains(mainSrc, "Frame time exceeded 16ms");
|
||||
|
||||
printf("step200_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -424,7 +424,7 @@ Make Whetstone usable by everyone and fast with large files.
|
||||
- "Reopen in Structured Mode" option if user wants full features
|
||||
*Modifies:* buffer opening logic, `panels/StatusBarPanel.h`
|
||||
|
||||
- [ ] **Step 200: Startup and panel performance**
|
||||
- [x] **Step 200: Startup and panel performance**
|
||||
Faster startup and smoother runtime:
|
||||
- Lazy-load panels: only parse/render on first show
|
||||
- Background grammar loading (don't block editor open for tree-sitter init)
|
||||
|
||||
Reference in New Issue
Block a user