From e919cffa2eaf2d757e1297849d85566e2355ba9b Mon Sep 17 00:00:00 2001 From: boxp Date: Thu, 26 Mar 2026 17:19:26 +0900 Subject: [PATCH] Add startup profiling option --- .../startup-profiling-option/plan.md | 23 +++++++ src/ceeker/core.clj | 7 ++- src/ceeker/tui/app.clj | 63 +++++++++++++++++-- test/ceeker/core_test.clj | 12 +++- test/ceeker/tui/app_test.clj | 50 +++++++++++++++ 5 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 docs/project_docs/startup-profiling-option/plan.md diff --git a/docs/project_docs/startup-profiling-option/plan.md b/docs/project_docs/startup-profiling-option/plan.md new file mode 100644 index 0000000..e74f5b5 --- /dev/null +++ b/docs/project_docs/startup-profiling-option/plan.md @@ -0,0 +1,23 @@ +# startup-profiling-option Plan + +## Goal + +`ceeker` の起動時に、明示オプション指定時だけ +セットアップ区間の計測ログを stderr へ出せるようにする。 + +## Scope + +- CLI に `--startup-profile` を追加 +- `start-tui!` の初期化処理を計測 + - `create-terminal` + - `create-watcher` + - `start-pane-checker` + - 合計時間 +- 通常起動時の挙動は変えない + +## Tests + +- `test/ceeker/core_test.clj` + - 新オプションが parse できること +- `test/ceeker/tui/app_test.clj` + - オプション有効時だけ計測ログが出ること diff --git a/src/ceeker/core.clj b/src/ceeker/core.clj index 8cb529b..35061bb 100644 --- a/src/ceeker/core.clj +++ b/src/ceeker/core.clj @@ -15,7 +15,9 @@ (def cli-options [["-h" "--help" "Show help"] ["-V" "--version" "Show version"] - [nil "--exit-on-jump" "Exit after a successful jump"]]) + [nil "--exit-on-jump" "Exit after a successful jump"] + [nil "--startup-profile" + "Log startup profiling to stderr"]]) (defn- usage "Returns usage string." @@ -132,5 +134,6 @@ :else (do (tui/start-tui! nil - {:exit-on-jump (:exit-on-jump options)}) + {:exit-on-jump (:exit-on-jump options) + :startup-profile (:startup-profile options)}) (System/exit 0)))))) diff --git a/src/ceeker/tui/app.clj b/src/ceeker/tui/app.clj index 6ccaf69..50611b0 100644 --- a/src/ceeker/tui/app.clj +++ b/src/ceeker/tui/app.clj @@ -128,6 +128,56 @@ (let [w (.getWidth terminal)] (if (pos? w) w 120))) +(defn- elapsed-ms + "Returns elapsed milliseconds since started-at." + [started-at] + (/ (- (System/nanoTime) started-at) 1000000.0)) + +(defn- timed-step + "Runs f and returns its result plus elapsed time." + [f] + (let [started-at (System/nanoTime) + result (f)] + {:result result + :elapsed-ms (elapsed-ms started-at)})) + +(defn- log-startup-profile! + "Logs startup profiling summary to stderr." + [{:keys [create-terminal create-watcher + start-pane-checker total]}] + (binding [*out* *err*] + (println + (str "ceeker: startup-profile " + "create-terminal=" + (format "%.2fms" create-terminal) + " create-watcher=" + (format "%.2fms" create-watcher) + " start-pane-checker=" + (format "%.2fms" start-pane-checker) + " total=" + (format "%.2fms" total))))) + +(declare create-watcher-for) + +(defn- setup-runtime + "Creates startup resources and optionally logs timings." + [state-dir startup-profile?] + (let [started-at (System/nanoTime) + terminal-step (timed-step input/create-terminal) + watcher-step (timed-step + #(create-watcher-for state-dir)) + checker-step (timed-step + #(start-pane-checker! state-dir))] + (when startup-profile? + (log-startup-profile! + {:create-terminal (:elapsed-ms terminal-step) + :create-watcher (:elapsed-ms watcher-step) + :start-pane-checker (:elapsed-ms checker-step) + :total (elapsed-ms started-at)})) + {:terminal (:result terminal-step) + :watcher (:result watcher-step) + :stop-ch (:result checker-step)})) + (defn- get-terminal-height "Gets the current terminal height from a JLine terminal." [^org.jline.terminal.Terminal terminal] @@ -321,19 +371,20 @@ (defn start-tui! "Runs the TUI application loop. opts may include :exit-on-jump to quit after a - successful jump." + successful jump and :startup-profile to log + startup timings." ([] (start-tui! nil)) ([state-dir] (start-tui! state-dir {})) ([state-dir opts] - (let [terminal (input/create-terminal) - w (create-watcher-for state-dir) - stop-ch (start-pane-checker! state-dir) + (let [{:keys [terminal watcher stop-ch]} + (setup-runtime state-dir + (:startup-profile opts)) exit-on-jump? (:exit-on-jump opts)] (try - (tui-loop terminal w state-dir exit-on-jump?) + (tui-loop terminal watcher state-dir exit-on-jump?) (finally (async/close! stop-ch) (print "\033[2J\033[H") (flush) - (watcher/close-watcher w) + (watcher/close-watcher watcher) (input/close-terminal terminal)))))) diff --git a/test/ceeker/core_test.clj b/test/ceeker/core_test.clj index 1ec27d7..8bcf411 100644 --- a/test/ceeker/core_test.clj +++ b/test/ceeker/core_test.clj @@ -2,7 +2,8 @@ (:require [ceeker.core :as core] [ceeker.tui.view :as view] [clojure.string :as str] - [clojure.test :refer [deftest is testing]])) + [clojure.test :refer [deftest is testing]] + [clojure.tools.cli :as cli])) (deftest version-is-loaded (testing "version reads from CEEKER_VERSION resource" @@ -87,3 +88,12 @@ (is (str/includes? (view/render [] 0 120 :auto) "View:Auto")) (is (str/includes? (view/render [] 0 120 :table) "View:Table")) (is (str/includes? (view/render [] 0 120 :card) "View:Card")))) + +(deftest cli-options-include-startup-profile + (testing "--startup-profile is parsed as a boolean option" + (let [{:keys [options errors]} + (cli/parse-opts ["--startup-profile"] + core/cli-options + :in-order true)] + (is (nil? errors)) + (is (true? (:startup-profile options)))))) diff --git a/test/ceeker/tui/app_test.clj b/test/ceeker/tui/app_test.clj index 3898bde..9cbc4b7 100644 --- a/test/ceeker/tui/app_test.clj +++ b/test/ceeker/tui/app_test.clj @@ -2,7 +2,10 @@ (:require [ceeker.tmux.pane :as pane] [ceeker.tui.app :as app] [ceeker.tui.filter :as f] + [ceeker.tui.input] + [ceeker.tui.watcher] [clojure.core.async :as async] + [clojure.string :as str] [clojure.test :refer [deftest is testing]])) (deftest test-handle-search-key-backspace-delete @@ -331,6 +334,53 @@ (is (= count-at-stop @call-count) "no more checks after stop"))))))) +(deftest test-start-tui-logs-startup-profile-when-enabled + (testing "start-tui! logs setup timings to stderr" + (let [err (java.io.StringWriter.)] + (binding [*err* err] + (with-redefs [ceeker.tui.app/create-watcher-for + (fn [_] :watcher) + ceeker.tui.app/start-pane-checker! + (fn [_] :stop-ch) + ceeker.tui.app/tui-loop + (fn [& _]) + ceeker.tui.input/create-terminal + (fn [] :terminal) + ceeker.tui.input/close-terminal + (fn [_]) + ceeker.tui.watcher/close-watcher + (fn [_]) + async/close! + (fn [_])] + (app/start-tui! nil {:startup-profile true}))) + (let [output (str err)] + (is (str/includes? output "ceeker: startup-profile")) + (is (str/includes? output "create-terminal=")) + (is (str/includes? output "create-watcher=")) + (is (str/includes? output "start-pane-checker=")) + (is (str/includes? output "total=")))))) + +(deftest test-start-tui-skips-startup-profile-when-disabled + (testing "start-tui! does not log timings without option" + (let [err (java.io.StringWriter.)] + (binding [*err* err] + (with-redefs [ceeker.tui.app/create-watcher-for + (fn [_] :watcher) + ceeker.tui.app/start-pane-checker! + (fn [_] :stop-ch) + ceeker.tui.app/tui-loop + (fn [& _]) + ceeker.tui.input/create-terminal + (fn [] :terminal) + ceeker.tui.input/close-terminal + (fn [_]) + ceeker.tui.watcher/close-watcher + (fn [_]) + async/close! + (fn [_])] + (app/start-tui! nil {}))) + (is (= "" (str err)))))) + ;; --- exit-on-jump tests --- (deftest test-handle-enter-key-returns-jumped-true