Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
fe440b2
perf(state): isolate remote tag discovery from local repository loads
Havunen Oct 16, 2026
aaaa1bf
perf(state): refresh history on activation only when metadata changes
Havunen Oct 16, 2026
63c791c
fix(ui): keep usable repository tabs quiet during background refresh
Havunen Oct 16, 2026
5777319
perf(git): reuse filesystem and stat caches during status scans
Havunen Oct 16, 2026
3f20b07
feat(profiling): trace startup stages and measure interactive readiness
Havunen Oct 16, 2026
bfb28b9
perf(fonts): skip installed-font scans for native system UI aliases
Havunen Oct 16, 2026
714df19
Merge remote-tracking branch 'origin/dev' into performance_investigation
Havunen Oct 16, 2026
670b07a
chore(ci): account for startup diagnostics in the host source budget
Havunen Oct 16, 2026
a362d8f
perf(git): skip redundant status checks for empty submodule checkouts
Havunen Oct 16, 2026
c880ce3
perf(state): defer selected-path reads until activation is checked
Havunen Oct 16, 2026
43655ce
Avoid selected-path preparation for unchanged repository tabs
Havunen Oct 16, 2026
8acbe0f
Skip empty linked-worktree scans on unchanged activation
Havunen Oct 16, 2026
f63fd16
Add benchmark-only native status stage profiling
Havunen Oct 16, 2026
d8e213a
Match Windows repository paths in profiling witnesses
Havunen Oct 16, 2026
8b6338b
Merge latest dev and adapt status refresh test fixtures
Havunen Oct 16, 2026
397a4e9
Merge branch 'dev' into performance_investigation
Havunen Oct 16, 2026
5354dfb
Fix font startup tracing after the dev merge
Havunen Oct 16, 2026
0e92403
Allow session lock tests to wait through parallel writers
Havunen Oct 16, 2026
ba6a443
Merge branch 'dev' into performance_investigation
Havunen Oct 16, 2026
0412b57
Match only filter attributes when selecting Windows status workers
Havunen Oct 17, 2026
9a1c77c
Skip line-statistics jobs for clean status snapshots
Havunen Oct 17, 2026
77b9e6f
Derive the remote metadata worker name from product identity
Havunen Oct 17, 2026
324a58f
Deduplicate aliased control-file reads in activation snapshots
Havunen Oct 17, 2026
3edce21
Track discovery link targets without scanning unrelated directories
Havunen Oct 17, 2026
e4b3350
Avoid Git subprocesses for conventional single-worktree listings
Havunen Oct 17, 2026
5bd43e6
Reuse status footer positions across unchanged diff renders
Havunen Oct 17, 2026
57658da
Merge latest dev file-history tab into performance investigation
Havunen Oct 17, 2026
72ddcda
Wake Windows Git command waiters when the process exits
Havunen Oct 17, 2026
0a60728
Make activation directory-link fixtures independent of path aliases
Havunen Oct 17, 2026
e0c0356
Build activation file-link targets with native path separators
Havunen Oct 17, 2026
ba8b31b
Cancel obsolete large-file tool probes and reject stale results
Havunen Oct 17, 2026
e240618
Restore Windows sleep polling after cancellation latency regression
Havunen Oct 17, 2026
1633633
Defer automatic large-file tool discovery until a repository opens
Havunen Oct 17, 2026
1289ff4
Preserve Git configuration errors in native worktree listings
Havunen Oct 17, 2026
4e5c2d4
Skip redundant Git metadata traversal with recursive Windows watchers
Havunen Oct 17, 2026
265840a
Preallocate bounded activation control-file reads
Havunen Oct 18, 2026
b21ad32
Filter hooks directory timestamp echoes from Git refreshes
Havunen Oct 18, 2026
a8e2cea
Merge latest dev into performance investigation
Havunen Oct 18, 2026
af50f65
perf(gix): reuse attribute source descriptors for unchanged indexes
Havunen Oct 18, 2026
7da5d96
perf(theme): skip custom catalog scans for bundled key checks
Havunen Oct 18, 2026
f0f2e8f
Merge latest dev into performance investigation
Havunen Oct 19, 2026
848514d
Merge branch 'dev' into performance_investigation
Havunen Oct 20, 2026
6b1f187
perf(windows): add startup capture helpers and measured baseline
Havunen Oct 20, 2026
23c4ec0
perf(windows): defer unused DX12 smoothing pipelines
Havunen Oct 20, 2026
1a2d25c
perf(startup): overlap repository bootstrap with window initialization
Havunen Oct 20, 2026
b37e714
perf(windows): compile ordinary DX12 pipelines in parallel
Havunen Oct 20, 2026
1da63f8
Pin GPUI to upstream lazy DX12 shader initialization
Havunen Oct 20, 2026
c55d94a
Adopt completed Git probes while native windows initialize
Havunen Oct 20, 2026
7ebb3e9
Pin GPUI to deferred Windows DX11 support-device creation
Havunen Oct 20, 2026
e41fe6c
Document measured Windows startup tradeoffs and remaining renderer costs
Havunen Oct 20, 2026
afcb771
Use opaque Windows surfaces to avoid first-present alpha reconfiguration
Havunen Oct 20, 2026
c6f19db
Pin GPUI to demand-driven filters and coalesced Windows startup resizes
Havunen Oct 20, 2026
ebcf9d4
Adopt completed Git discovery before attaching the empty Home view
Havunen Oct 20, 2026
71db65d
Defer automatic LFS and annex discovery through the initial frame
Havunen Oct 20, 2026
5a48946
Reuse validated ancestor metadata during watcher input expansion
Havunen Oct 20, 2026
8bd0600
Exercise watcher ancestor replacement without Windows symlink privileges
Havunen Oct 20, 2026
86d78e6
Batch startup diagnostic records to avoid redirected stderr stalls
Havunen Oct 20, 2026
c4b00cb
Document Windows startup fixes, 496 ms confirmation, and timing variance
Havunen Oct 20, 2026
29171fa
use latest gpui-ce fork
Havunen Oct 20, 2026
6626031
Pin gpui-ce to precompiled DX12 shaders
Havunen Oct 21, 2026
12a05b4
Update rustls-webpki and webpki-roots for new advisories
Havunen Oct 21, 2026
052ed5a
Re-run CI after flaky native test jobs
Havunen Oct 21, 2026
669d668
Add Linux renderer preference persistence
Havunen Oct 21, 2026
a9928b1
Update GPUI interaction style method calls
Havunen Oct 21, 2026
1569edc
Add Linux graphics renderer settings
Havunen Oct 21, 2026
8947331
Upgrade GPUI and renderer dependencies
Havunen Oct 21, 2026
d7da8cf
Merge branch 'dev' of github.com:Auto-Explore/GitComet into linux-ren…
Havunen Oct 21, 2026
69a8543
Update GPUI source contracts
Havunen Oct 21, 2026
f9e3b5b
Explain Linux renderer startup failures
Havunen Oct 21, 2026
03bada4
Merge #633: startup performance investigation
Havunen Oct 21, 2026
cae8988
Merge #645: precompiled DX12 shaders
Havunen Oct 21, 2026
c3b7be4
Merge #648: GPUI wgpu 30 upgrade
Havunen Oct 21, 2026
5fa67a6
Remove the Linux renderer choice
Havunen Oct 21, 2026
7fd1917
Pin gpui-ce with wgpu 30 and precompiled shaders
Havunen Oct 21, 2026
0e3f221
List no submodules in the popover test repository
Havunen Oct 21, 2026
b7ed6bb
Retry the watcher ancestor-replacement setup across clock ticks
Havunen Oct 21, 2026
439f7e1
Swap the parent in the watcher ancestor-replacement test
Havunen Oct 21, 2026
bd602be
Canonicalize tempdir paths in window bootstrap tests
Havunen Oct 21, 2026
354fe96
removed design doc
Havunen Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 86 additions & 88 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ futures = "0.3.34"
gix = { version = "0.89.0", default-features = false, features = ["max-performance-safe", "comfort", "basic", "status", "blame", "sha1", "sha256"] }
# Already in the tree via gix; Git-compatible quoting for attribute patterns.
gix-quote = "0.10.0"
gpui = { package = "gpui-ce", git = "https://github.com/Havunen/gpui-ce.git", rev = "0665fb07d59cf7af185e58f44b9f890590abe204" }
gpui_platform = { package = "gpui_ce_platform", git = "https://github.com/Havunen/gpui-ce.git", rev = "0665fb07d59cf7af185e58f44b9f890590abe204", features = ["font-kit"] }
gpui_wgpu = { package = "gpui_ce_wgpu", git = "https://github.com/Havunen/gpui-ce.git", rev = "0665fb07d59cf7af185e58f44b9f890590abe204" }
gpui_parley = { package = "gpui_ce_parley", git = "https://github.com/Havunen/gpui-ce.git", rev = "0665fb07d59cf7af185e58f44b9f890590abe204", default-features = false }
gpui_shared_string = { package = "gpui_ce_shared_string", git = "https://github.com/Havunen/gpui-ce.git", rev = "0665fb07d59cf7af185e58f44b9f890590abe204" }
gpui_sum_tree = { package = "gpui_ce_sum_tree", git = "https://github.com/Havunen/gpui-ce.git", rev = "0665fb07d59cf7af185e58f44b9f890590abe204" }
gpui = { package = "gpui-ce", git = "https://github.com/Havunen/gpui-ce.git", rev = "1b6f3cedafcd97fa15e6bc5fdbb8f34cc805afc0" }
gpui_platform = { package = "gpui_ce_platform", git = "https://github.com/Havunen/gpui-ce.git", rev = "1b6f3cedafcd97fa15e6bc5fdbb8f34cc805afc0", features = ["font-kit"] }
gpui_wgpu = { package = "gpui_ce_wgpu", git = "https://github.com/Havunen/gpui-ce.git", rev = "1b6f3cedafcd97fa15e6bc5fdbb8f34cc805afc0" }
gpui_parley = { package = "gpui_ce_parley", git = "https://github.com/Havunen/gpui-ce.git", rev = "1b6f3cedafcd97fa15e6bc5fdbb8f34cc805afc0", default-features = false }
gpui_shared_string = { package = "gpui_ce_shared_string", git = "https://github.com/Havunen/gpui-ce.git", rev = "1b6f3cedafcd97fa15e6bc5fdbb8f34cc805afc0" }
gpui_sum_tree = { package = "gpui_ce_sum_tree", git = "https://github.com/Havunen/gpui-ce.git", rev = "1b6f3cedafcd97fa15e6bc5fdbb8f34cc805afc0" }
image = "0.25.10"
fast_image_resize = { version = "6.1.0", features = ["image"] }
# Already in the dependency graph; merges sorted reference streams lazily.
Expand Down
48 changes: 33 additions & 15 deletions crates/gitcomet-app/src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ pub(crate) fn run_mode(
#[cfg(not(feature = "ui-gpui-runtime"))]
let () = extensions;

install_configured_git_executable_preference(&mode);
{
let _stage = gitcomet_core::startup_trace::span("git_executable_preference");
install_configured_git_executable_preference(&mode);
}
#[cfg(feature = "ui-gpui-runtime")]
let backend = || build_backend(&repository_options, backend_override.clone());
#[cfg(not(feature = "ui-gpui-runtime"))]
Expand Down Expand Up @@ -261,7 +264,11 @@ fn run_browser(
}

let path = browser_instance::normalize_browser_path(path);
let browser_open_target = gitcomet_state::session::load()
let session = {
let _stage = gitcomet_core::startup_trace::span("browser_session_load");
gitcomet_state::session::load()
};
let browser_open_target = session
.browser_open_target
.as_deref()
.and_then(gitcomet_ui_gpui::BrowserOpenTarget::from_key)
Expand All @@ -270,18 +277,21 @@ fn run_browser(
path: path.clone(),
target: browser_open_target,
};
let mut browser_instance =
match browser_instance::start_or_forward(initial_browser_request.clone()) {
Ok(browser_instance::StartResult::Forwarded) => return exit_code::SUCCESS,
Ok(browser_instance::StartResult::Primary(primary)) => Some(primary),
Err(err) => {
eprintln!(
"Could not initialize the {name} browser-instance broker; \
let instance_result = {
let _stage = gitcomet_core::startup_trace::span("browser_instance_broker");
browser_instance::start_or_forward(initial_browser_request.clone())
};
let mut browser_instance = match instance_result {
Ok(browser_instance::StartResult::Forwarded) => return exit_code::SUCCESS,
Ok(browser_instance::StartResult::Primary(primary)) => Some(primary),
Err(err) => {
eprintln!(
"Could not initialize the {name} browser-instance broker; \
launching this process independently: {err}"
);
None
}
};
);
None
}
};
let browser_requests = browser_instance
.as_mut()
.and_then(browser_instance::PrimaryBrowserInstance::take_requests);
Expand All @@ -295,17 +305,25 @@ fn run_browser(
let _ = std::thread::Builder::new()
.name("git-probe".into())
.spawn(move || {
let _stage = gitcomet_core::startup_trace::span("git_runtime_probe_background");
let _ = probe.run();
});
}

let startup_crash_report = crashlog::take_startup_report();
let startup_crash_report = {
let _stage = gitcomet_core::startup_trace::span("startup_crash_report");
crashlog::take_startup_report()
};
if let Some(report) = startup_crash_report.as_ref() {
// Keep the recovery path visible even if WSLg cannot create
// a window for the in-app notification.
print_startup_crash_report_hint(report);
}
if let Err(err) = crashlog::begin_session() {
let crash_session_result = {
let _stage = gitcomet_core::startup_trace::span("crashlog_begin_session");
crashlog::begin_session()
};
if let Err(err) = crash_session_result {
eprintln!("Failed to create {name} crash-recovery marker before UI launch: {err}");
}
let startup_report = startup_crash_report.map(|report| gitcomet_ui_gpui::StartupCrashReport {
Expand Down
12 changes: 10 additions & 2 deletions crates/gitcomet-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ impl AppLaunch {

/// Runs with `args` (including the program name) and returns the exit code.
pub fn run_with_args(self, args: Vec<OsString>) -> i32 {
gitcomet_core::startup_trace::mark("app_entry");
let Self {
identity,
about,
Expand All @@ -157,9 +158,16 @@ impl AppLaunch {
}

#[cfg(feature = "ui-gpui-runtime")]
crashlog::install();
{
let _stage = gitcomet_core::startup_trace::span("crashlog_install");
crashlog::install();
}

dispatch(cli::parse_cli(args, &about), on_prepare, move |mode| {
let parsed = {
let _stage = gitcomet_core::startup_trace::span("parse_cli");
cli::parse_cli(args, &about)
};
dispatch(parsed, on_prepare, move |mode| {
launch::run_mode(mode, extensions, repository_options, backend)
})
}
Expand Down
1 change: 1 addition & 0 deletions crates/gitcomet-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test-support = []

[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
sysinfo = { workspace = true }
tempfile = { workspace = true }

Expand Down
1 change: 1 addition & 0 deletions crates/gitcomet-base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod loadable;
pub mod op_trace;
pub mod panic;
pub mod platform;
pub mod startup_trace;

pub use loadable::Loadable;

Expand Down
187 changes: 187 additions & 0 deletions crates/gitcomet-base/src/startup_trace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! Opt-in startup stage timings, available before the native UI exists.
//!
//! Set `GITCOMET_STARTUP_TRACE` to a local file path. Each line is JSON; begin
//! records survive a stalled stage and end records include its duration. The
//! file is appended, with a process id on every record. No repository paths,
//! environment values or command arguments are recorded.

use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::{Mutex, OnceLock};
use std::time::Instant;

static TRACE: OnceLock<Option<Trace>> = OnceLock::new();

struct Trace {
origin: Instant,
file: Mutex<File>,
}

fn trace() -> Option<&'static Trace> {
TRACE
.get_or_init(|| {
let path = std::env::var_os("GITCOMET_STARTUP_TRACE").filter(|p| !p.is_empty())?;
let origin = Instant::now();
match OpenOptions::new().create(true).append(true).open(path) {
Ok(file) => Some(Trace {
origin,
file: Mutex::new(file),
}),
Err(error) => {
crate::write_stderr_line(format_args!("Cannot open startup trace: {error}"));
None
}
}
})
.as_ref()
}

pub fn is_enabled() -> bool {
trace().is_some()
}

/// A timestamp relative to the earliest startup trace call in this process.
pub fn mark(name: &'static str) {
if let Some(trace) = trace() {
trace.write("mark", name, None);
}
}

/// Times a synchronous stage. Keep the guard alive only for that stage, not
/// for the whole event loop. Disabled tracing does not read the clock.
#[must_use = "the startup stage ends when its guard drops"]
pub fn span(name: &'static str) -> Span {
Span(trace().map(|trace| {
trace.write("begin", name, None);
(name, Instant::now())
}))
}

pub struct Span(Option<(&'static str, Instant)>);

impl Drop for Span {
fn drop(&mut self) {
if let Some((name, started)) = self.0
&& let Some(trace) = trace()
{
trace.write("end", name, Some(started.elapsed().as_secs_f64() * 1_000.0));
}
}
}

impl Trace {
fn write(&self, kind: &str, name: &str, duration_ms: Option<f64>) {
let thread = std::thread::current();
let record = record(
kind,
name,
self.origin.elapsed().as_secs_f64() * 1_000.0,
duration_ms,
thread.name(),
);
if let Ok(mut file) = self.file.lock() {
let _ = write_record(&mut *file, &record);
}
}
}

fn write_record(writer: &mut impl Write, record: &serde_json::Value) -> std::io::Result<()> {
// Value's Display implementation streams many tiny writes. Assemble one
// complete record so tracing does not add a filesystem call per JSON token.
let line = format!("{record}\n");
writer.write_all(line.as_bytes())
}

fn record(
kind: &str,
name: &str,
elapsed_ms: f64,
duration_ms: Option<f64>,
thread: Option<&str>,
) -> serde_json::Value {
serde_json::json!({
"kind": kind,
"name": name,
"elapsed_ms": elapsed_ms,
"duration_ms": duration_ms,
"pid": std::process::id(),
"thread": thread,
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn stage_records_are_json_with_distinct_elapsed_and_stage_duration() {
let value = record("end", "native_application", 125.0, Some(95.0), Some("main"));
let parsed: serde_json::Value = serde_json::from_str(&value.to_string()).unwrap();
assert_eq!(parsed["elapsed_ms"], 125.0);
assert_eq!(parsed["duration_ms"], 95.0);
assert_eq!(parsed["pid"], std::process::id());
assert_eq!(parsed["name"], "native_application");
}

#[test]
fn trace_writes_each_record_as_a_complete_line_without_waiting_for_shutdown() {
let temporary = tempfile::NamedTempFile::new().unwrap();
let trace = Trace {
origin: Instant::now(),
file: Mutex::new(temporary.reopen().unwrap()),
};
trace.write("begin", "font\"catalog\n日本語", None);
let first = std::fs::read_to_string(temporary.path()).unwrap();
assert_eq!(first.lines().count(), 1);
let first: serde_json::Value = serde_json::from_str(first.trim_end()).unwrap();
assert_eq!(first["name"], "font\"catalog\n日本語");
trace.write("end", "font\"catalog\n日本語", Some(2.0));
let contents = std::fs::read_to_string(temporary.path()).unwrap();
let rows: Vec<serde_json::Value> = contents
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["kind"], "begin");
assert_eq!(rows[0]["name"], "font\"catalog\n日本語");
assert!(rows[0]["duration_ms"].is_null());
assert_eq!(rows[1]["duration_ms"], 2.0);
}

#[test]
fn trace_serializes_one_complete_record_per_write_without_changing_timestamps() {
#[derive(Default)]
struct Writes(Vec<Vec<u8>>);
impl std::io::Write for Writes {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.push(bytes.to_vec());
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

let value = record(
"end",
"quote\" slash\\ newline\n雪",
125.125,
Some(95.25),
Some("worker\tthread"),
);
let mut writer = Writes::default();
write_record(&mut writer, &value).unwrap();
assert_eq!(
writer.0.len(),
1,
"JSON tokens must not cause separate writes"
);
let bytes = &writer.0[0];
assert_eq!(bytes.last(), Some(&b'\n'));
assert_eq!(bytes.iter().filter(|byte| **byte == b'\n').count(), 1);
let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
assert_eq!(parsed, value);
assert_eq!(parsed["elapsed_ms"], 125.125);
assert_eq!(parsed["duration_ms"], 95.25);
}
}
29 changes: 23 additions & 6 deletions crates/gitcomet-core/src/large_file_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//! PATH, which is what every filter, hook and command will use.

use crate::process::{
bytes_to_text_preserving_utf8, current_git_runtime, git_command_for_preference, probe_output,
GitExecutablePreference, GitRuntimeState, bytes_to_text_preserving_utf8, current_git_runtime,
git_command_for_preference, probe_output,
};
use crate::services::CancellationToken;
pub use crate::signing_tools::SigningToolAvailability as ToolAvailability;
Expand Down Expand Up @@ -33,14 +34,27 @@ impl Default for LargeFileToolsState {
pub fn detect_large_file_tools_cancellable(
cancellation: &CancellationToken,
) -> LargeFileToolsState {
let runtime = current_git_runtime();
detect_large_file_tools_for_runtime_cancellable(&current_git_runtime(), cancellation)
}

/// Probe the runtime captured when the request was created, even if the
/// configured executable changes before its worker starts.
pub fn detect_large_file_tools_for_runtime_cancellable(
runtime: &GitRuntimeState,
cancellation: &CancellationToken,
) -> LargeFileToolsState {
detect_for_runtime_with_factory(runtime, &git_command_for_preference, cancellation)
}

fn detect_for_runtime_with_factory(
runtime: &GitRuntimeState,
git: &(dyn Fn(&GitExecutablePreference) -> Command + Sync),
cancellation: &CancellationToken,
) -> LargeFileToolsState {
if !runtime.is_available() || cancellation.is_cancelled() {
return LargeFileToolsState::default();
}
detect_large_file_tools_with(
&|| git_command_for_preference(&runtime.preference),
cancellation,
)
detect_large_file_tools_with(&|| git(&runtime.preference), cancellation)
}

/// Injectable factory for deterministic tests.
Expand Down Expand Up @@ -157,3 +171,6 @@ mod tests {
assert_eq!(state, LargeFileToolsState::default());
}
}

#[cfg(test)]
mod runtime_tests;
Loading
Loading