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 69093a3..ae2f0b2 100644 --- a/src/ceeker/core.clj +++ b/src/ceeker/core.clj @@ -20,7 +20,9 @@ :parse-fn keyword :validate [#{:auto :table :card} "Must be one of: auto, table, card"]] - [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." @@ -98,6 +100,13 @@ (doseq [e errors] (println e))) (System/exit 1)) +(defn- tui-opts + "Builds TUI opts from parsed CLI options." + [options] + {:exit-on-jump (:exit-on-jump options) + :startup-profile (:startup-profile options) + :initial-display-mode (:view options)}) + ;; musl? is evaluated at AOT compile time (macro expansion time). ;; For musl static builds, set CEEKER_STATIC=true CEEKER_MUSL=true ;; when running `clojure -T:build uber` to produce a musl-compatible binary. @@ -137,6 +146,5 @@ :else (do (tui/start-tui! nil - {:exit-on-jump (:exit-on-jump options) - :initial-display-mode (:view options)}) + (tui-opts options)) (System/exit 0)))))) diff --git a/src/ceeker/tui/app.clj b/src/ceeker/tui/app.clj index 8534289..95feb97 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,23 +371,24 @@ (defn start-tui! "Runs the TUI application loop. opts may include :exit-on-jump to quit after a - successful jump and :initial-display-mode to + successful jump, :startup-profile to log + startup timings, and :initial-display-mode to set the startup view." ([] (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) initial-display-mode (:initial-display-mode opts :auto)] (try - (tui-loop terminal w state-dir exit-on-jump? + (tui-loop terminal watcher state-dir exit-on-jump? initial-display-mode) (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 48d3c92..0b0ada7 100644 --- a/test/ceeker/core_test.clj +++ b/test/ceeker/core_test.clj @@ -89,6 +89,15 @@ (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)))))) + (deftest cli-accepts-view-option (testing "--view accepts supported startup views" (doseq [mode ["auto" "table" "card"]] diff --git a/test/ceeker/tui/app_test.clj b/test/ceeker/tui/app_test.clj index 2e58de3..df62b0e 100644 --- a/test/ceeker/tui/app_test.clj +++ b/test/ceeker/tui/app_test.clj @@ -5,6 +5,7 @@ [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 @@ -333,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