Repository: jpmonettas/flow-storm-debugger Branch: master Commit: e64bd0cac4b3 Files: 112 Total size: 1.2 MB Directory structure: gitextract_xzq9jm4n/ ├── .dir-locals.el ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── Makefile ├── Readme.md ├── UNLICENSE ├── build.clj ├── deps.edn ├── docs/ │ ├── _config.yml │ ├── dev_notes.md │ ├── high_level_diagram.drawio │ ├── index.html │ ├── related-research-and-tools.md │ ├── run_configs.drawio │ ├── test-cases.org │ ├── timeline.drawio │ ├── user_guide.adoc │ └── user_guide.html ├── examples/ │ └── plugins/ │ └── basic-plugin/ │ ├── deps.edn │ └── src/ │ └── flow_storm/ │ └── plugins/ │ └── timelines_counters/ │ ├── all.clj │ ├── runtime.cljc │ └── ui.clj ├── llm-prompt.txt ├── package.json ├── resources/ │ └── flowstorm/ │ ├── fonts/ │ │ └── LICENSE.txt │ └── styles/ │ ├── font-size-lg.css │ ├── font-size-md.css │ ├── font-size-sm.css │ ├── font-size-xl.css │ ├── styles.css │ ├── theme_dark.css │ └── theme_light.css ├── scripts/ │ ├── flow-clj │ ├── gsettings │ └── mock-gnome.sh ├── shadow-cljs.edn ├── src-dbg/ │ └── flow_storm/ │ └── debugger/ │ ├── docs.clj │ ├── events_processor.clj │ ├── events_queue.clj │ ├── main.clj │ ├── repl/ │ │ ├── core.clj │ │ └── nrepl.clj │ ├── runtime_api.clj │ ├── state.clj │ ├── tutorials/ │ │ └── basics.clj │ ├── ui/ │ │ ├── browser/ │ │ │ └── screen.clj │ │ ├── commons.clj │ │ ├── components.clj │ │ ├── data_windows/ │ │ │ ├── data_windows.clj │ │ │ ├── visualizers/ │ │ │ │ └── oscilloscope.clj │ │ │ └── visualizers.clj │ │ ├── docs/ │ │ │ └── screen.clj │ │ ├── flows/ │ │ │ ├── bookmarks.clj │ │ │ ├── call_tree.clj │ │ │ ├── code.clj │ │ │ ├── components.clj │ │ │ ├── functions.clj │ │ │ ├── general.clj │ │ │ ├── multi_thread_timeline.clj │ │ │ ├── printer.clj │ │ │ ├── screen.clj │ │ │ └── search.clj │ │ ├── main.clj │ │ ├── outputs/ │ │ │ └── screen.clj │ │ ├── plugins.clj │ │ ├── tasks.clj │ │ └── utils.clj │ ├── user_guide.clj │ └── websocket.clj ├── src-dev/ │ ├── dev.clj │ ├── dev_tester.clj │ ├── dev_tester.cljs │ ├── dev_tester_12.clj │ ├── logging.properties │ └── user.clj ├── src-inst/ │ ├── data_readers.clj │ └── flow_storm/ │ ├── api.clj │ ├── api.cljs │ ├── jobs.cljc │ ├── nrepl/ │ │ └── middleware.clj │ ├── ns_reload_utils.clj │ ├── preload.cljs │ ├── remote_websocket_client.clj │ ├── remote_websocket_client.cljs │ ├── runtime/ │ │ ├── debuggers_api.cljc │ │ ├── events.cljc │ │ ├── indexes/ │ │ │ ├── api.cljc │ │ │ ├── form_registry.cljc │ │ │ ├── protocols.cljc │ │ │ ├── storm_form_registry.clj │ │ │ ├── thread_registry.cljc │ │ │ ├── timeline_index.cljc │ │ │ ├── total_order_timeline.cljc │ │ │ └── utils.cljc │ │ ├── outputs.cljc │ │ ├── types/ │ │ │ ├── bind_trace.cljc │ │ │ ├── expr_trace.cljc │ │ │ ├── fn_call_trace.cljc │ │ │ └── fn_return_trace.cljc │ │ └── values.cljc │ ├── storm_api.clj │ ├── storm_preload.cljs │ └── tracer.cljc ├── src-shared/ │ └── flow_storm/ │ ├── eql.cljc │ ├── form_pprinter.clj │ ├── json_serializer.clj │ ├── json_serializer.cljs │ ├── state_management.cljc │ ├── types.cljc │ └── utils.cljc └── tests.edn ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dir-locals.el ================================================ ((clojure-mode . ((cider-clojure-cli-aliases . "dev:dev-tools:storm") (clojure-dev-menu-name . "flow-storm-dev-menu") (cider-jack-in-nrepl-middlewares . ("refactor-nrepl.middleware/wrap-refactor" "cider.nrepl/cider-middleware" "flow-storm.nrepl.middleware/wrap-flow-storm"))))) ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [jpmonettas] # patreon: # Replace with a single Patreon username # open_collective: # Replace with a single Open Collective username # ko_fi: # Replace with a single Ko-fi username # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry # liberapay: # Replace with a single Liberapay username # issuehunt: # Replace with a single IssueHunt username # otechie: # Replace with a single Otechie username # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ .clj-kondo target .cpcache .nrepl-port .shadow-cljs .cljs_node_repl out/ node_modules public/js/ /.calva/ /.lsp/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## master (unreleased) ### New Features ### Changes ### Bugs fixed - Fix multi thread timeline viewer running out of colors - Fix flow-storm.runtime.indexes/detailed-total-order-timeline ## 4.5.9 (28-10-2025) ### New Features ### Changes - Improving flow-storm.api/prob-ref ### Bugs fixed - Fixing oscilloscope ## 4.5.8 (13-10-2025) ### New Features ### Changes ### Bugs fixed - Fix seqable visualizer when clicking on empty rows ## 4.5.7 (03-10-2025) ### New Features ### Changes - Use java.util.logging to log, so it can be disabled with logging.properties ### Bugs fixed ## 4.5.6 (24-09-2025) ### New Features ### Changes - Upgrading hansel to 0.1.90 to improve objects methods names tracing ### Bugs fixed ## 4.5.5 (10-09-2025) ### New Features ### Changes ### Bugs fixed - Fix .nrepl-port not found exception ## 4.5.4 (9-09-2025) ### New Features ### Changes ### Bugs fixed - Fix heap reporting for ClojureScript (where heap data is currently missing) ## 4.5.3 (02-09-2025) ### New Features - When starting the ui for remote debugging, when port not provided check .nrepl-port ### Changes - Update hansel to 0.1.87 ### Bugs fixed ## 4.5.2 (01-09-2025) ### New Features - Added JVM option flowstorm.heapLimit which can be set also from the UI to automatically stop recording when the heap reaches the set size in megabytes ### Changes - JVM option flowstorm.throwOnTraceLimit has been renamed to flowstorm.throwOnLimit - Update hansel to 0.1.85 ### Bugs fixed - Fix exceptions instrumentation in vanilla for non #trace ones ## 4.5.1 (26-08-2025) ### New Features - Data Windows on-create can be called with :preferred-size :small - Adds oscilloscope support for DW :preferred-size :small - Add :pre-require to flow-storm.debugger.main/start-debugger for easier data window visualizers in ClojureScript ### Changes - Perf improvement: reduce garbage created by keep-timeline-sub-range - Perf improvement: improve how we send navs-refs for datawindows which are nil most of the time ### Bugs fixed ## 4.5.0 (05-08-2025) ### New Features - A better oscilloscope data window ### Changes - Limit the exceptions we are tracking on the UI to 100 so we don't make the UI super slow when too many exceptions are fired - Improve quick jump box for same function fired on many threads ### Bugs fixed - Fixed transients inspection ## 4.4.6 (10-06-2026) ### New Features - Added llm-prompt.txt that can be used to instruct LLMs on how to use FlowStorm - Added utilities api functions useful for llm-prompt.txt ### Changes ### Bugs fixed ## 4.4.5 (06-06-2025) ### New Features ### Changes ### Bugs fixed - Fix shallow-indexed aspect extractor ## 4.4.4 (26-05-2025) ### New Features ### Changes ### Bugs fixed - Report interruptible task failed messages - Don't swallow the exception on :dbg ## 4.4.3 (21-05-2025) ### New Features ### Changes ### Bugs fixed - Fix data windows default visualizers system ## 4.4.2 (13-05-2025) ### New Features ### Changes - Make :preview the default visualizer for numbers - Improved thread breakpoints UX/UI - All api calls from UI have now a timeout, configurable via flowstorm.uiTimeoutMillis ### Bugs fixed - Fix fn calls list styling - Fix Browser's function 'Break' button styling ## 4.4.1 (03-05-2025) ### New Features ### Changes - Locals context menu changed "Define all frame vars" to "Define all" which only defines the visible ones. This is to make the future less confusing, specially in the presence of loops. - Sort locals by symbol name ### Bugs fixed - Fix locals display inside loops ## 4.4.0 (17-04-2025) ### New Features - Check for `:flow-storm.power-step/skip` value when power stepping ### Changes - Internal refactor ### Bugs fixed - Extra styles should be applied at the end ## 4.3.0 (29-03-2025) ### New Features ### Changes - Reduced memory footprint by ~10% (jvm17) by removing this-idx from FnCall, ExprExec, FnReturn, FnUnwind entries - Programmable API BREAKING: - removed indexes-api/entry-idx now that each entry doesn't know its own index - (indexes-api/get-fn-call timeline entry) -> (indexes-api/get-fn-call timeline idx) - (indexes-api/get-sub-form timeline entry) -> (indexes-api/get-sub-form timeline idx) ### Bugs fixed ## 4.2.2 (18-03-2025) ### New Features - Add flowstorm.autoUpdateUI jvm opt - Add flowstorm.threadTraceLimit jvm opt - Add flowstorm.throwOnThreadLimit jvm opt - Add flowstorm.callTreeUpdate jvm opt ### Changes - Disable eql-query-pprint by default - Perf improvements. We were calling val-pprint in a bunch of places where v-ref :val-preview could be used ### Bugs fixed - Catch exceptions on preview build when print fails ## 4.2.1 (11-03-2025) ### New Features - Accept multiple flowstorm.plugins.namespaces.* jvm props that will get merged ### Changes - Make the locals pane a table, so columns can be resized and searched ### Bugs fixed ## 4.2.0 (25-02-2025) ### New Features - Basic plugins system - Enable/disable instrumentation from the UI ### Changes - Disable preview pprints by default for speed. You can still enable them via the Config menu. ### Bugs fixed ## 4.1.2 (11-02-2025) ### New Features ### Changes - Make scope visualizer a "rolling scope" - Show tree-view childs truncation message inside the tree-view instead of a popup alert - Upgrade to hansel 0.1.84 which doesn't depend on core.async ### Bugs fixed ## 4.1.1 (29-01-2025) ### New Features - Configurable pprint previews menu for slow to pprint values cases - Very crude zoom-in/zoom-out on scope visualizer ### Changes - Make *out* and *err* print panel font monospaced - Make printer support printing multi-line strings when printing strings ### Bugs fixed - Fix printer removing after flow cleanning - Fix data-window eql extractor so it doesn't break on infinite sequences ## 4.1.0 (01-01-2025) ### New Features - Add configurable auto jump to exceptions - New eql-query-pprint visualizer - New webpage visualizer - Add support for setting the visualizer via flow-storm.api/data-window-push-val - Add debugger/bookmark to set bookmarks from code and also quickly jump to the first one - Enable multiple ClojureScript runtimes <> multiple debuggers via "flowstorm_ws_port" ### Changes - Configurable thread tab auto-update every 1sec (on by default) - Remove middleware dependency on cider-nrepl-middleware - Aspect extractors signature deprecation. Now it recieves the object and a map with extras ### Bugs fixed - Fix taps "search value on flows" - Fix middleware not working with nrepl > 1.3.0-beta2 - Fix middleware for ClojureScript - Fix identity and equality powerstepping for unwinds ## 4.0.2 (25-11-2024) ### New Features ### Changes - Change flow exceptions combo to only show exception root instead of all unwinds ### Bugs fixed ## 4.0.1 (12-11-2024) ### New Features ### Changes ### Bugs fixed - Fix middleware for Cider Storm ## 4.0.0 (11-11-2024) ### New Features - DataWindow system - Outputs tool (Clojure only) ### Changes - Improved flow search for DataWindows support - Update javafx to "21.0.4-ea+1" ### Bugs fixed - Fix Quick Jump on multiple flows - Fix UI displaying of multi line strings in varios places - Fix printers enable state on printers screen open ## 3.17.4 (24-09-2024) ### New Features ### Changes ### Bugs fixed - Fix thread trace count not updating on thread tab refresh - Fix ctx menu showing on forms background with left click - For ns reload functionality don't read forms with *read-eval* false ## 3.17.3 (27-08-2024) ### New Features ### Changes - Use quoted-string-split on editor open commands ### Bugs fixed ## 3.17.2 (06-08-2024) ### New Features ### Changes - Improved multi-thread timeline colors - Do not deref all derefables automatically, just atoms, refs, agents and vars and pending realized ones. Leave the reast to snapshot-value system ### Bugs fixed - Do not deref delays when tracing values ## 3.17.1 (26-07-2024) ### New Features - Add before-reload and after-reload hooks for the namespace reloading utilities ### Changes - Update tutorial ### Bugs fixed - Improve ns auto reload for namespaces loaded with load-file ## 3.17.0 (23-07-2024) ### New Features - Make Search, Printer and Multi-thread-timeline tools work per flow - Pake Printer use the multi-thread timeline if available to allow for thread interleaving print debugging - New fn-call power stepper - Optional automatic namespaces reload after changing prefixes for storm ### Changes - Flows UI refactor - Make stack click jump to beg of the fn instead of prev step ### Bugs fixed ## 3.16.0 (03-07-2024) ### New Features - Add printer support to print on all threads - Add printers transform expression (Clojure only) ### Changes - UI refactor for toolbars - UI refactor for printers and multi-thread timeline - Set a UI limit of 200 on the Exceptions menu entries to improve UI responsiveness under deep recursions exceptions. ### Bugs fixed - Fix #181 close children windows if main window closed - Fix out of boundaries on step-prev-over and step-next-over ## 3.15.8 (21-06-2024) ### New Features ### Changes - Add recorded threads counter on the threads menu button - Clean initialization console logs for ClojureScript ### Bugs fixed - Fix remote debugging for vanilla - Fix windows paths handling in middleware ## 3.15.7 (13-06-2024) ### New Features ### Changes ### Bugs fixed - Fix FlowStorm not starting in OSX because of wrong taskbar icon url ## 3.15.6 (13-06-2024) ### New Features - Add jump to first and last occurrences buttons to power stepping controls ### Changes - #rtrace now clears the current recording flow before running again - Upgrade ikonli-javafx to 12.3.1 - Add FlowStorm icon to the toolbar and taskbar - Namespace FlowStorm resources so they don't collide with other resources ### Bugs fixed - Fix thread-id lost from thread tab after tab refresh - Fix running with nrepl >= 1.2 ## 3.15.5 (11-05-2024) ### New Features ### Changes ### Bugs fixed - Make the code stepper the default tab instead of the tree ## 3.15.4 (06-05-2024) ### New Features ### Changes ### Bugs fixed - Fix auto tab switch on code jump ## 3.15.3 (02-05-2024) ### New Features - Add Goto file:line on the menu - Show open in editor for any form that contains line meta ### Changes - Show value inspector `def` on the same screen - Remove unnamed anonymous functions from Quick Jump - Remove docs builder (now on dyna-spec) - Remove tools.build as a dependency - Make the coed stepper the default tab instead of the tree ### Bugs fixed ## 3.15.2 (18-04-2024) ### New Features - Timelines notification updates and thread refresh button ### Changes ### Bugs fixed ## 3.15.1 (16-04-2024) ### New Features ### Changes ### Bugs fixed - Don't run #rtrace when recording is paused - Fix vanilla-instrument-var for clojurescript - Fix #rtrace for clojurescript ## 3.15.0 (16-04-2024) ### New Features - Implement storm instrumentation management in the browser - Add "Copy qualified function symbol" and "Copy function calling form" to form menu - Add only-functions? to multi-thread timeline ### Changes - The record flow selector combo is now next to the tabs - The threads selector is now a menu button instead of a list view to gain some UI space - Automatically open first recorded thread - Improved graphical tutorial - Help menu with tutorial and user's guide - Don't start recording by default ### Bugs fixed ## 3.14.0 (30-03-2024) ### New Features - Add support to open forms in editors - Add an option to set threads limit to throw - There is a new, much more powerfull global search system instead of the old per timeline search. - An improved Flows system that allows the user to record into multiple flows. - Implement an improved loop navigation system ### Changes - Improve pprint panes. Now all of the also show exceptions - #rtrace0 ... #rtrace5 reader tags were removed since they aren't needed anymore with the new flow system ### Bugs fixed - Theme dialog panes - Fix inspector power stepper ## 3.13.1 (21-03-2024) ### New Features ### Changes - Bring back the clear recordings button to the toolbar - A faster search system for ClojureScript (for every functionality that searches on the timeline) ### Bugs fixed ## 3.13.0 (19-03-2024) ### New Features - Implemented thread trace limit as a fuse for infinite loops/recursion - All possibly expensive slow operations are now cancellable. This includes : - List functions calls - List prints with the printer - Values search - Multi-thread timeline - All power stepping tools - Quick jump - All functions that collect from the timeline will report as they run, no need to wait to the end. This includes : - List functions calls - List prints with the printer - Multi-thread timeline - Add a menu bar to help with discoverability - Add functions calls ret render checkbox that update on change ### Changes - Change Exceptions from a ComboBox to MenuButton - Functions calls auto update on selection ### Bugs fixed ## 3.12.2 (05-03-2024) ### New Features ### Changes ### Bugs fixed - Fix functions lists for nil returns ## 3.12.1 (05-03-2024) ### New Features ### Changes - Improved functions pane. - Change stack double click behavior as per #150 - Display bookmarks and inspector windows centered on main window - Display all dialogs centered on main window ### Bugs fixed ## 3.12.0 (15-02-2024) ### New Features - Add identity-other-thread power stepper ### Changes - Big refactor with a ton of improvements to the repl API ### Bugs fixed ## 3.11.2 (13-02-2024) ### New Features ### Changes ### Bugs fixed - Fix quick jump ## 3.11.1 (08-02-2024) ### New Features ### Changes - Better execution highlighting system - Browser and functions calls list views update as they change with the keyboard ### Bugs fixed - Fix multi-thread timeline search - Close context menu if there is one already open ## 3.11.0 (01-02-2024) ### New Features - Add exceptions display ### Changes - :ex captured exception system removed - superseded by unwind tracing ### Bugs fixed - [IMPORTANT!] Fix timeline structure when functions unwind (requires ClojureStorm >= 1.11.1-19) - Fix value inspector stack showing val instead of key ## 3.10.0 (29-01-2024) ### New Features - Add a timeline index column to the bookmarks table - Add a thread timeline index column to the multi-thread timeline - New same-coord and custom-same-coord power steppers - New context menu to find recordings from unhighlighted text ("Jump forward here", etc) ### Changes - Make tooltips have a 400ms delay instead of default 1000ms - Display timeline indexes zero based (used to be 1 based) to be less confusing when using the repl api - Add thread-ids everywhere thread-names show - Show form line next to form namespace if known - New stepper buttong layout - Centralize bookmarks system (one bookmark system for all flows and threads) - Step over doesn't stop at function boundaries anymore - Improved value inspector performance on remote runtimes (via :val-preview) ### Bugs fixed - Pasting in quick jump box doesn't fire autocomplete - Quick jump should create and/or focus the tab - Fix tree view freezes when a node contains too many childs - Fix quick-jump on Clojure remote ## 3.9.1 (12-01-2024) ### New Features - Add print-wrap controls on panes ### Changes - Don't automatically switch to the Taps tab on new taps ### Bugs fixed - Don't trace false as nil - Update hansel to 0.1.81 to fix browser var instrumentation in cljc ## 3.9.0 (19-12-2023) ### New Features - Add a bookmarking system - Add navigation undo/redo system ### Changes - Upgrade JavaFX to 21.0.1 - Push minimal supported JDK version to 17 with a path for using it with 11 - Improve keyboard event handling system to support different layouts - Improve following current selected expression - Remove double scrolling in code panes - Make code stack pane jump a double-click - Support multiple debugger instances running at the same time. Useful for debugging multiple build in cljs. ### Bugs fixed - Fix "Add to prints" not showing on Vanilla - Enter on time box focus the code ## 3.8.6 (17-11-2023) ### New Features ### Changes - Don't print handled exception error messages on std-err since it messes up cider ### Bugs fixed - Capture exceptions on cljs remote connect - [Remote] Don't crash the debugger if there is an exception initializing the RT through the repl ## 3.8.5 (10-11-2023) ### New Features ### Changes ### Bugs fixed - Patch for clojure.pprint bug https://ask.clojure.org/index.php/13455/clojure-pprint-pprint-bug-when-using-the-code-dispatch-table ## 3.8.4 (09-11-2023) ### New Features - Ctrl-f copies the current qualified funcion symbol to the clipboard - Ctrl-Shift-f copies the current function call form - Right clicking on a tree node now shows "Copy qualified function symbol" - Right clicking on a tree node now shows "Copy function calling form" ### Changes - Big codebase refactor for make it cleaner - Improved search functionality (faster and with better UX) ### Bugs fixed - Fix functions list not showing entire functions names when they are large ## 3.8.3 (25-10-2023) ### New Features - Add dynamic font inc/dec and theme rotation ### Changes ### Bugs fixed ## 3.8.2 (23-10-2023) ### New Features ### Changes - Upgrading j-system-theme-detector to 3.8.1 to fix a NPE - Downgrading JavaFX to 19.0.2 since >20 needs JDK>=17 and we still want JDK11 ### Bugs fixed ## 3.8.1 (19-10-2023) ### New Features ### Changes - Improved code highlighter. Replaces JavaFX standard TextFlow with RichTextFx CodeArea for improved performance. - Change hansel to com.github.flow-storm/hansel 0.1.79 for the organization move ### Bugs fixed - Fix #98 Stepping over big forms is very slow ## 3.7.5 (02-10-2023) ### New Features - Add function call limits ### Changes - Disable functionality that doesn't make sense under storm when working under ClojureStorm or ClojureScriptStorm - Improve Printer thread selection so you don't need to constantly re-select thread ### Bugs fixed ## 3.7.4 (27-09-2023) ### New Features - Add flow-storm.storm-preload for ClojureScriptStorm ### Changes - Improved initialization system for remote debugging - Reuduce callstack tree nodes args print level and depth for perf (specially on remotes) ### Bugs fixed ## 3.7.3 (10-09-2023) ### New Features - Add multimethod dispatch-val to stack pane ### Changes - Upgrade hansel to 0.1.78 ### Bugs fixed - Fix printer goto location without thread selection ## 3.7.1 (06-09-2023) ### New Features ### Changes ### Bugs fixed - Fix ClojureScript double require issue - Fix java.util.ConcurrentModificationException when building timeline ## 3.7.1 (21-08-2023) ### New Features - Implement quickjump - Unblock all breakpoint blocked threads with toolbar and keyboard ### Changes ### Bugs fixed - Fix enabling/disabling of thread breakpoints ## 3.7.0 (15-08-2023) ### New Features - Add "Search value on Flows" to taps - Add "Timeline tool" implementation - Add power stepping to stepping controls - Add "Printer tool" implementation ### Changes ### Bugs fixed - Fix NPE after closing thread tab ## 3.6.10 (19-07-2023) ### New Features ### Changes - Upgrade to hansel 0.1.74 for a couple of bug fixes. Check hansel changelog. ### Bugs fixed ## 3.6.9 (06-07-2023) ### New Features ### Changes ### Bugs fixed - Fix ability to capture what happens before the debugger connects on ClojureScript ## 3.6.8 (03-07-2023) ### New Features - Code stepping follow value allows you to step to the next/prev expression that evaluates to the same value. Useful for understanding how values flow through programs - Value inspector follow value, same as before. - New code stepping search tool ### Changes - The call stack tree search tool was removed since it was buggy and hard to fix because of how javaFx lazy TreeView works ### Bugs fixed ## 3.6.7 (30-06-2023) ### New Features ### Changes - Upgrade to hansel 0.1.69 with improved coordinate system ### Bugs fixed - Fix call tree highlight current frame ## 3.6.6 (29-06-2023) ### New Features - Add debugger window title configurable via :title and flowstorm.title prop - Add support for debugging litteral maps and sets of any size ### Changes - Upgrades to hansel 0.1.65 and supports new coordinate system - Reintroduced #rtrace ^{:thread-trace-limit N} ### Bugs fixed - issues/65 GUI icon hover making the icon unreadable ## 3.6.5 (16-06-2023) ### New Features ### Changes - Add flow-storm-find-flow-fn-call to nrepl middleware ### Bugs fixed ## 3.6.4 (13-06-2023) ### New Features - Add flow-storm.nrepl.middleware for editors integration ### Changes - Improve step out functionality ### Bugs fixed ## 3.6.3 (06-06-2023) ### New Features ### Changes - Fix flow-storm.runtime.values/value-type for sorte-maps - Upgrade hansel to 0.1.63 ### Bugs fixed - Fix step over for the (map some-fn) etc cases ## 3.6.2 (01-06-2023) ### New Features ### Changes - Exclude org.slf4j/slf4j-nop ### Bugs fixed ## 3.6.1 (30-05-2023) ### New Features ### Changes - Make pprint panes a TextArea so we can copy it's content - Upgrade hansel to 0.1.60 to fix ClojureScript namespace instrumentation issue ### Bugs fixed ## 3.6.0 (19-05-2023) ### New Features - Add functions list refresh button - Add step-prev-over and step-next-over buttons and keybindings - Pprint panel now display value type - Add define all current frame bindings - Improve auto-scrolling when stepping on the code tool ### Changes - Keep functions and tree state when switching tabs - Much improved value inspector - Update hansel to 0.1.56 so #trace (deftest ...) works - BREAKING! flow-storm.runtime.values/snapshot-value defmethod was replaced by flow-storm.runtime.values/SnapshotP protocol for performance ### Bugs fixed - Fix goto-location - Respect flowstorm.startRecording before :dbg on ClojureStorm ## 3.5.1 (01-05-2023) ### New Features - Add "Highlight current frame" on the call tree tool - Add stack tab to code stepping tool - Add a result pane on the functions list tool - Add double click on calls tree tool node steps code - Control recording from the UI - Add threads breakpoints - Add basic keyboard support - Allow DEF button functionality to specify a NS ### Changes - Made search functionality faster and simplified it's code - #rtrace automatically opens the code stepping tool in the last position - Upgrade hansel to 0.1.54 - Signal error when trying to use #rtrace with ClojureStorm ### Bugs fixed - Don't crash if tracer functions are called before the system is fully started - Fix Clojure remote debugging race condition - Keep the thread list split pane size correct after window resize ## 3.4.1 (17-04-2023) ### New Features ### Changes ### Bugs fixed - [CRITICAL] For remote connections fix repl-watchdog spamming the repl ## 3.4.0 (16-04-2023) ### New Features - Add support for ClojureStorm - Add a separate threads list and thread tabs contain names - Add step up button - Redesign Flows tools UX ### Changes - A bunch of performance improvements ### Bugs fixed - Many bug fixes (sorry for the low detail, this is a big release) ## 3.3.325 (09-04-2023) ### New Features ### Changes - Exclude guava as a transitive dep in tools.build since it breaks shadow-cljs 2.21.0 - Upgrade hansel to 0.1.50 ### Bugs fixed ## 3.3.320 (16-02-2023) ### New Features ### Changes - Upgrade openjfx to 19.0.2.1 - Upgrade hansel to 0.1.46 ### Bugs fixed ## 3.3.315 (30-01-2023) ### New Features ### Changes - Upgrade to hansel 0.1.42 which contains a couple of bug fixes ### Bugs fixed ## 3.3.313 (29-01-2023) ### New Features ### Changes ### Bugs fixed - Update to hansel 0.1.38 which contains a couple of bug fiexs ## 3.3.309 (29-12-2022) ### New Features ### Changes - Improve docs file generation. Generated docs will be put into flow-docs.edn instead of samples.edn since it is less likely to collide. Also the data format has been improved for extensibility ### Bugs fixed ## 3.3.307 (26-12-2022) ### New Features ### Changes - Update hansel dependency to 0.1.35 ### Bugs fixed ## 3.3.303 (20-12-2022) ### New Features ### Changes ### Bugs fixed - Handle OS theme detector exceptions ## 3.3.301 (16-12-2022) ### New Features ### Changes - Update hansel to 0.1.31 which contains a critical bug - Improve value inspector styles ### Bugs fixed - Fix docs examples display ### 3.3.295 (14-12-2022) ## New Features - Add Flow Docs - Generate projects functions documentation by sampling their executions. ### Changes ### Bugs fixed - Fix a ConcurrentModificationException on debuggers_api/reference_frame_data! ## 3.2.283 (29-11-2022) ## New Features - Add browser var recursive instrumentation ### Changes ### Bugs fixed - Update to hansel 0.1.22 to fix ClojureScript go blocks instrumentation ## 3.2.271 (16-11-2022) ## New Features - New tap button allows you to tap> any value from the UI (nice to integrate with other tooling like portal) - New locals "tap value" allows you to tap> any locals ### Changes - Migrate to hansel for instrumentation (should be much better than previous instrumentation system) - Value inspector navigation bar now shows keys instead of val text ### Bugs fixed - Fix automatic [un]instrumentation watcher ## 3.1.263 (18-10-2022) ## New Features ### Changes ### Bugs fixed - Fix clojure instrument entire namespace for non libraries ## 3.1.261 (18-10-2022) ## New Features ### Changes - Remove flow-storm hard dependency on org.clojure/clojurescript artifact. Will lazy require when needed for ClojureScript, assuming the dependency will be provided ### Bugs fixed ## 3.1.259 (17-10-2022) ## New Features - Add #rtrace ^{:thread-trace-limit X} where X can be a integer. Execution will throw after tracing X times for a trace. Useful for debugging possibly infinite loops - Add support for snapshoting mutable values via flow-storm.runtime.values/snapshot-value multimethod - Add #tap-stack-trace, to tap the current stack trace wherever you add it - Add support for core.async/go blocks instrumentation - Add Ctrl+MouseWheel on forms to step prev/next ### Changes - Immediately highlihgt the first trace when creating a flow in the debugger - Remove unnecessary first-fn-call-event ### Bugs fixed - Alt+Tab now works on MacOs for switching between the debugger and the repl windows (thanks to Lucy Wang @lucywang000) - Fix extend-protocol and extend-type instrumentations for ClojureScript - Fix instrumentation breaking variadic functions in ClojureScript ## 3.0.236 (7-10-2022) ## New Features ### Changes - Improves theming - Fix dynamic vars not being re-evaluated as dynamic in cljs ### Bugs fixed - Fix dynamic vars not being re-evalueated as dynamic in ClojureScript - Fix timeout issues on remote connection ## 3.0.231 (6-10-2022) ## New Features - Add `Instrument form without bindings` to form context menu - Add got to last trace on code loops context-menu ### Changes - Full [un]instrumentation synchronization between the browser and #trace (Clojure only) - Now you can [un]instrument single vars from the browser, even if they where defined at the repl (Clojure only) - Improved Single var [un]instrumentation from the browser (Clojure and ClojureScript) ### Bugs fixed - Fix show-error on the clojure local path ## 3.0.216 (29-9-2022) ## New Features - flow-storm.api/cli-run now supports :flow-id key ### Changes ### Bugs fixed - Windows pprint form problem - Bunch of minor small bug fixes ## 3.0.208 (28-9-2022) ## New Features ### Changes - Instrummented code should run much faster due to removed unnecessary runtime ctx rebinding ### Bugs fixed - Fix browser navigation after instrumentation bug ## 3.0.198 (24-9-2022) ## New Features - Automatic event retention. For ClojureScript or remote Clojure you don't need to remote-connect by hand when you want to capture traces before the debugger is connected. Just need to require flow-storm.api on your main. - Automatic ui cleaning on reconnection ### Changes - Remote connection now accepts :debugger-host and :runtime-host keys (check out the documentation) ### Bugs fixed - Fix the more button on value inspector - Fix for infinite sequence handling ## 3.0.188 (22-9-2022) ## New Features - Automatic remote connection management. The re-connect button was removed from the toolbar since it isn't needed anymore ### Changes ### Bugs fixed - Fix javafx platform not initialized exception when there is a error connecting to a repl ## 3.0.184 (21-9-2022) ## New Features ### Changes ### Bugs fixed - Add support for remote debugging without repl connection (clojure and clojurescript) - Show nrepl errors on the UI - Fix ClojureScript re-run flow - Fix a deadlock caused by the event system ## 3.0.173 (15-9-2022) ## New Features 3.0 is a full redesign! So it is full of changes, and fixes. Most remarkable things are : - ClojureScript is feature par with Clojure now, so every feature is available to both languages. - Remote debugging can be accomplished by just connecting to nrepl server (socket repl support on the roadmap) - A programable API (https://jpmonettas.github.io/flow-storm-debugger/user_guide.html#_programmable_debugging) - Enables the posibility to integrate it with IDEs/editors ### Changes ### Bugs fixed ## 2.3.141 (15-08-2022) ## New Features * Add inspect for locals * Can jump to any index by typing the idx number in the thread controls ### Changes * Locals print-length is now 20 and print-level 5 * Make the value inspector show more info on dig nodes ### Bugs fixed ## 2.3.131 (10-08-2022) ## New Features * Add a proper lazy and recursive value inspector * Add tap tool (support for tap>) * New functions for shutting down the debugger and connections gracefully. When starting with `flow-storm.api/local-connect` or `flow-storm.api/remote-connect` you can shut it down with `flow-storm.api/stop` When starting a standalone debugger with `flow-storm.debugger.main/start-debugger` you can shutdown with `flow-storm.debugger.main/stop-debugger` * Add support for light and dark themes, in selected or automatic mode. Checkout the user guide for more info. (thanks to Liverm0r!) * Thread tabs can be closed and reordered ### Changes * The entire debugger was refactored to manage state with mount ### Bugs fixed * Fix #28 - Callstack tree args and ret listviews should expand to the bottom * Fix #34 - instrument-forms-for-namespaces not instrumenting (def foo (fn [...] ...)) ## 2.2.114 (07-07-2022) ## New Features * Add bottom bar progress indicator when running commands * Add reload tree button on callstack tree tabs * Add search bar to functions list * This release also contains big internal refactors to make the codebase cleaner and more efficient ### Changes * Automatically deref all reference values in traces * Automatically change to flow tab on new flow ### Bugs fixed ## 2.2.99 (10-06-2022) ## New Features * Add jump to first and last traces on thread controls (useful for exceptions debugging) * Add print-level and print-meta controls on pprint value panels * Improve re-run flow UX * Namespace instrumentation now accepts :verbose? to log known and unknown instrumentation errors details * Add flow-storm.api/uninstrument-forms-for-namespaces to undo instrument-form-for-namespaces instrumentation * Add ctx menu on locals to define vars from values * Add browser namespaces instrumentation/uninstrumentation * Add browser instrumentation synchronization (for everything but #trace) * Add #rtrace0 ... #rtrace5, like #rtrace but with different flow-ids * Add double clicking on flows functions window executes show function calls ### Changes * Remove flow-storm.api/run since #rtrace(runi) is enough * Flows functions window now have checkboxes for selecting fncall arguments to print ### Bugs fixed * Fix re run flow for #rtrace case * Fix local binding instrumentation and debugging ## 2.2.68 (10-06-2022) ## New Features * Add def value button on every value panel to define the value so you can work with it at the repl * Add namespaces browser with instrumentation capabilities ### Changes ### Bugs fixed ## 2.2.64 (09-06-2022) ## New Features * Add conditional tracing via #ctrace and ^{:trace/when ...} meta ### Changes ### Bugs fixed ## 2.2.59 (06-06-2022) ## New Features ### Changes ### Bugs fixed * Fix run-command for the local connection path ## 2.2.57 (06-06-2022) ## New Features * Add Clojurescript support * Remote debugging via `flow-storm.api/remote-connect` and `flow-storm.api/cli-run` ### Changes * `flow-storm.api/cli-run` now accepts :host and :port ### Bugs fixed ## 2.0.38 (02-05-2022) ## New Features * Add styles customization via a user provided styles file * The debugger can instrument and debug itself ### Changes ### Bugs fixed ## 2.0.0 (18-04-2022) ================================================ FILE: Makefile ================================================ .PHONY: clean docs test lint-dbg lint-inst install-dbg install-inst deploy-dbg deploy-inst docs: docs/user_guide.adoc asciidoctorj -b html5 -o docs/user_guide.html docs/user_guide.adoc clean: clj -T:build clean test: clj -M:test:dev unit-clj lint: clj-kondo --config .clj-kondo/config.edn --lint src-dbg src-shared src-inst flow-storm-dbg.jar: clj -T:build jar-dbg flow-storm-inst.jar: clj -T:build jar-inst install-dbg: flow-storm-dbg.jar mvn install:install-file -Dfile=target/flow-storm-dbg.jar -DpomFile=target/classes/META-INF/maven/com.github.flow-storm/flow-storm-dbg/pom.xml install-inst: flow-storm-inst.jar mvn install:install-file -Dfile=target/flow-storm-inst.jar -DpomFile=target/classes/META-INF/maven/com.github.flow-storm/flow-storm-inst/pom.xml deploy-dbg: mvn deploy:deploy-file -Dfile=target/flow-storm-dbg.jar -DrepositoryId=clojars -DpomFile=target/classes/META-INF/maven/com.github.flow-storm/flow-storm-dbg/pom.xml -Durl=https://clojars.org/repo deploy-inst: mvn deploy:deploy-file -Dfile=target/flow-storm-inst.jar -DrepositoryId=clojars -DpomFile=target/classes/META-INF/maven/com.github.flow-storm/flow-storm-inst/pom.xml -Durl=https://clojars.org/repo ================================================ FILE: Readme.md ================================================ # Flow-storm debugger This is the central repository for [FlowStorm](http://www.flow-storm.org/) an omniscient time travel debugger for Clojure and ClojureScript. ![demo](./docs/images/screenshot.png) There are two ways of using it : - [With ClojureStorm](https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_clojurestorm) (recommended) : For dev, swap your Clojure compiler by ClojureStorm and get everything instrumented automatically - [Vanilla FlowStorm](https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_vanilla_flowstorm) : Just add FlowStorm to your dev classpath and instrument by re-evaluating forms ClojureStorm is a fork of the official Clojure compiler that adds automatic instrumentation so you don't need to think about it (you can still disable it when you don't need it). You use it by swapping the official Clojure compiler by ClojureStorm at dev time, using dev aliases or profiles. # Artifacts FlowStorm latest stable releases : - The complete debugger (includes `flow-storm-inst`) - `[com.github.flow-storm/flow-storm-dbg "4.5.9"]` - A slimmer version with no GUI, to use it for Clojure or ClojureScript remote debugging - `[com.github.flow-storm/flow-storm-inst "4.5.9"]` ClojureStorm latest stable releases : - Clojure 1.12 - `[com.github.flow-storm/clojure "1.12.4"]` - Clojure 1.11 - `[com.github.flow-storm/clojure "1.11.4-10"]` ClojureScriptStorm latest stable releases : - ClojureScript 1.12.116 - `[com.github.flow-storm/clojurescript "1.12.134-3"]` - ClojureScript 1.11.132 - `[com.github.flow-storm/clojurescript "1.11.132-9"]` # Prerequisites - jdk17+ (if you still need to run it with jdk11 take a look at [here](https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_run_with_jdk_11)) - clojure 1.11.0+ - clojure 1.10.* only supported if you use it from source, like `{:git/url "https://github.com/flow-storm/flow-storm-debugger" :git/sha "..."}` # QuickStart and Documentation If you want to use it with Clojure checkout the [Clojure QuickStart guide](https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_clojure) or the [ClojureScript QuickStart Guide](https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_clojurescript) if you are using ClojureScript. Please refer to the [user's guide](https://flow-storm.github.io/flow-storm-debugger/user_guide.html) for a list of features and how to use them. # ClojureStorm and ClojureScriptStorm *ClojureStorm* is a dev compiler. It is a fork of the official Clojure compiler enhanced with automatic instrumentation. To use it, you just swap it with your normal Clojure compiler at dev time (by using deps cli aliases or lein profiles) to improve your development experience, while making sure you use your normal compiler for everything else (tests and production). *ClojureScriptStorm* is the same as ClojureStorm but for ClojureScript, so a fork of the official ClojureScript compiler is enhanced with automatic instrumentation. ClojureStorm sources are here : https://github.com/flow-storm/clojure ClojureScriptStorm sources are here : https://github.com/flow-storm/clojurescript # Features Flow storm debugger is packed with a ton of features, which the [user's guide](https://flow-storm.github.io/flow-storm-debugger/user_guide.html) covers in detail. ## Information for developers If you want to enhance, fix, debug, or just learn about the internals of FlowStorm take a look at [here](./docs/dev_notes.md) ## Some demo videos (newers at the top) - [FlowStorm demo at Clojure Apropos](https://www.youtube.com/watch?v=a-PrBjlBdw8) - [ClojureScript compiler fun with FlowStorm](https://www.youtube.com/watch?v=YYHRx3EnPmg) - [Don't fear the storm](https://www.youtube.com/watch?v=CspQX_R0NbM) - [Clojure visual-tools 29 - FlowStorm 4.1 workflows](https://www.youtube.com/watch?v=9nY25hwzWRc) - [Clojure web apps with FlowStorm 3.17](https://www.youtube.com/watch?v=h8AFpZkAwPo) - [Reifying execution, the interactive programming missing piece](https://www.youtube.com/watch?v=BuSpMvVU7j4&t=1394s) - [FlowStorm printer demo](https://www.youtube.com/watch?v=06-MA4HSS24) - [Smashing a real ClojureScript bug with FlowStorm](https://www.youtube.com/watch?v=4VXT-RHHuvI) - [Debugging Clojure with FlowStorm 3.6](https://www.youtube.com/watch?v=Mmr1nO6uMzc) - [Searching and following values](https://www.youtube.com/watch?v=CwXhy-QsZHw) - [Show me your REPL episode](https://www.youtube.com/watch?v=2nH59edD5Uo) - [Debugging Clojure with FlowStorm](https://www.youtube.com/watch?v=PbGVTVs1yiU) - [Debugging ClojureScript with FlowStorm](https://www.youtube.com/watch?v=jMYl32lnMhI) - [Presentation at London Clojurians](https://www.youtube.com/watch?v=A3AzlqNwUXc) - [Flows basics](https://www.youtube.com/watch?v=YnpQMrkj4v8) - [Instrumenting libraries](https://youtu.be/YnpQMrkj4v8?t=332) - [Debugging the ClojureScript compiler](https://youtu.be/YnpQMrkj4v8?t=533) - [Browser](https://www.youtube.com/watch?v=cnLwRzxrKDk) - [Def button](https://youtu.be/cnLwRzxrKDk?t=103) ## FAQ ### Clojure has the repl, why does it need a debugger? In [this talk](https://www.youtube.com/watch?v=A3AzlqNwUXc&t=934s) I tried to argue that even as amazing as it is to have a repl to poke around, there are some inconveniences that I think can be greatly improved by a debugger. - Defining function parameters and locals with def (for sub form execution) isn't easy for complex values - When functions contains loops, maps, filters, etc with anonymous functions is hard to capture every value for further inspection. - Being Clojure a dynamic lang, running the program in my head isn't easy if I'm not familiar with the code base - Adding prints (or tap>) is inconvenient because you need to guess where the problem probably is first. - Debugging statements needs to be constantly typed and removed which gets repetitive and annoying - Exploring complex values at the console is tedious, that is why tools like portal, reveal, rebl, etc exist. Some of the issues there can be alleviated by adding libraries like scope capture, and portal but it isn't straight forward to integrate them and even if it was IMHO there is a lot to be gained from a proper integrated debugging system. So I want to stop guessing and a tool that allows me to see what is happening when a Clojure program runs, be it a small expression or an entire codebase. I want it to be useful for cases when I'm chasing a bug or when I just want to understand how something works. I also think some Clojure constraints, like immutability and being expression based, allows us to go beyond repl poking and steppers. We can record everything that happens when a program runs and then inspect the execution using multiple tools (being a stepper one of them) and fully integrating it with the repl which also enhance the repl poking experience. ### What's that magic? How does it work? The idea behind FlowStorm is pretty simple : - Instrument Clojure code - Run it - Record everything that happens on each thread during execution into timelines - Provide a GUI with multiple tools to explore the recorded values and execution flows The interesting part here I guess are instrumentation and recording. FlowStorm can instrument your code in two ways : - The ClojureStorm way (recommended for Clojure) which swaps your official Clojure compiler with a patched one (only for dev) that emits extra JVM bytecode everytime you compile something to record everything that is happening. This method provides automatic instrumentation everywhere, which is very practical. You still get to un-instrument things if you need to do things like measure performance, which isn't going to be accurate with the extra bytecode. - The vanilla way (can be used for Clojure and is the only option for ClojureScript), that just grabs the Clojure source expressions you are interested in, walks the AST, instruments it, and re-evals the instrumented version through the repl. You do this by using reader tags (like `#trace (defn foo [...] ...)`) or by using the FlowStorm browser tab to instrument entire namespaces. Doesn't matter which method instrumented the code, when it runs it will record every expression in a timeline. Because Clojure is expression based and most data is immutable, recording is just retaining JVM references together with the source coordinate of each expression. This is pretty fast and a simple way of recording execution. The timeline is a structure that provides efficient insertion, and efficient access sequentially and as a functions call tree. You can see some diagrams here : https://flow-storm.github.io/flow-storm-debugger/user_guide.html#_internals_diagrams_and_documentation ### Isn't recording everything too expensive? Like, does it work with something like a game loop? The answer here is it depends. First, not everything needs to be recorded all the time. FlowStorm provides easy controls for start/stop recording, instrumenting/un-instrumenting stuff, freeing recordings, and for keeping an eye on the heap space which you can also control via JVM parameters. For a lot of applications it is probably fine even if you keep recording everything. For applications that generate a lot of recordings you can keep the recording off and just enable it with one click right before executing the action you want to record, and then turn it off again. For things like game loops you can also use the thread breakpoints functionality which provides a way of pausing/resuming threads at specific points so you have more control on how they execute. Combining that with start/stopping recording is probably enough to debug this kind of application. ### Aren't omniscient debuggers slow? Omniscient debuggers have a reputation of being slow, and I think a bunch of them are. IMHO a lot of the slowness comes from how things need to be recorded and replayed on mutable languages. For Clojure, just retaining references is enough, and looking into a value at any point in time is just looking at it. Since most references point to immutable values, they don't need to be reconstructed by applying state changes for each step. There are mutable language omniscient debuggers that implement stepping backwards fast enough to be useful, but trying to implement things like searching or following values is probably going to be much slower since everything needs to be reconstructed at each step. ### How does it compare to other Clojure debuggers? There are many comparison points with different Clojure debugging tools, I'll try to list some of them here. If you feel something here is unfair or needs to be clarified please open an issue or submit a PR. Also if you would like to see here how it compares with any other debugging tool let me know. The first big difference is that FlowStorm works for Clojure and ClojureScript while most debuggers like Cider/VSCode/Cursive only work for Clojure. So once you learn to use FlowStorm you can use it with both languages. The second imho big difference is that most debugging tools are designed to understand small pieces of execution you could be interested in, while FlowStorm is designed to look at a program execution as a whole, to be used not only while chasing bugs but also as a development companion to help you reason about whatever system you are running. Most tools like Cider, Cursive or VScode debuggers are blocking steppers and require you to add breakpoints to specific places, which is okay if you have an idea already of where the bug could be located, but they fall short if you aren't sure, or you just want to use the tool to understand an entire codebase execution. #### FlowStorm VS Cursive (or Browser's for ClojureScript) debuggers I think the main difference here is FlowStorm being expression and value oriented while the Cursive and Browser's one being line and memory poking oriented, which I don't think is very useful in Clojure[Script]. If you want to experience what I mean, try to debug this exception using Cursive vs FlowStorm/Cider/VSCode : ``` (defn foo [n] (->> (range n) (filter odd?) (partition-all 2) (map second) (drop 10) (reduce +))) (foo 70) ``` On the other side, the nice thing about the Cursive debugger is that you can use it to step over Java code which is useful if you have a mixed lang codebase. But for those cases you can always use both. #### FlowStorm VS Cider/VSCode debuggers Cider and VSCode debuggers are steppers, so I'll only be comparing it against the stepping and value inspection capabilities of FlowStorm, but keep in mind that FlowStorm provides many more features than stepping. Cider provide some tracing capabilities to trace entire namespaces but it relies on printing to the console which I haven't found useful outside of very specific situations, so I don't think it is useful to understand an entire codebase. For the steppers, the main difference is that Cider/VSCode are blocking debuggers, which are nice in some situations but only provide stepping forward, while FlowStorm allows you to step in many different ways. Another difference is that currently on Cider/VSCode you can't step into functions unless you have previously instrumented them, which for the most isn't a problem in FlowStorm if you are using ClojureStorm. A nice thing about Cider/VSCode steppers is that you get to step over your editor source code, which can also be done in FlowStorm if you use an integration like CiderStorm. There is also some code that currently can't be stepped with Cider/VSCode that can be with FlowStorm. This is code that happens to be inside literal sets of maps with more than 8 keys. If you want to compare it try stepping code like this in both debuggers : ``` (defn bla [] {(+ 1 2) (first #{3}) 2 (+ 1 1) 4 (+ 2 2) 6 (+ 3 3) 8 (+ 4 4) 10 (+ 5 5) 12 (+ 6 6) 14 (+ 7 7) 16 (+ 8 8)}) (bla) ``` ## What to do when things don't work? Please create a [issue](https://github.com/flow-storm/flow-storm-debugger/issues) if you think you found a bug. If you are not sure you can ask in : - [#flow-storm slack channel](https://clojurians.slack.com/archives/C03KZ3XT0CF) - [github discussions](https://github.com/flow-storm/flow-storm-debugger/discussions) ## Acknowledgements Big thanks to [Roam Research](https://roamresearch.com/), [Nubank](https://nubank.com.br/) and all previous and current sponsors. ![demo](./docs/images/roam_research_logo.jpg) ![demo](./docs/images/nubank_logo.png) Thanks to [Cider](https://github.com/clojure-emacs/cider/) debugger for inspiration and some clever ideas for code instrumentation. ================================================ FILE: UNLICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: build.clj ================================================ (ns build (:require [clojure.tools.build.api :as b] [clojure.string :as str] [clojure.java.io :as io] [clojure.spec.alpha :as s])) (def version (or (System/getenv "VERSION") "4.5.9")) (def target-dir "target") (def class-dir (str target-dir "/classes")) (defn clean [_] (b/delete {:path target-dir})) (def aot-compile-nses "Precompile this so clojure storm can call flow-storm.storm-api functions at repl init and it doesn't slow down repl startup." ['flow-storm.storm-api 'flow-storm.utils 'flow-storm.api 'flow-storm.runtime.debuggers-api 'flow-storm.runtime.types.fn-call-trace 'flow-storm.runtime.indexes.timeline-index 'flow-storm.runtime.indexes.api 'flow-storm.runtime.indexes.protocols 'flow-storm.runtime.types.fn-return-trace 'flow-storm.runtime.events 'flow-storm.runtime.indexes.thread-registry 'flow-storm.runtime.indexes.form-registry 'flow-storm.runtime.values 'flow-storm.runtime.types.bind-trace 'flow-storm.runtime.types.expr-trace 'flow-storm.runtime.indexes.utils]) (def aot? (if-let [aot (System/getenv "AOT")] (read-string aot) ;; if no AOT value provided we default to true true)) (defn- check-jvm [] (let [jvm-version (-> (System/getProperty "java.specification.version") Integer/parseInt)] (when (>= jvm-version 21) (throw (ex-info "Not building with JVM >= 21 because of the SequencedCollection issue. See https://aphyr.com/posts/369-classnotfoundexception-java-util-sequencedcollection" {}))) (println "Building with JVM " jvm-version))) (defn jar-dbg [_] (check-jvm) (clean nil) (println "AOT compiling dbg : " aot?) (let [lib 'com.github.flow-storm/flow-storm-dbg basis (b/create-basis {:project "deps.edn"}) jar-file (format "%s/%s.jar" target-dir (name lib)) src-dirs ["src-dbg" "src-shared" "src-inst"]] (b/write-pom {:class-dir class-dir :lib lib :version version :basis basis :src-dirs src-dirs :pom-data [[:licenses [:license [:name "Unlicense"] [:url "http://unlicense.org/"]]]]}) (when aot? (b/compile-clj {:basis basis :src-dirs src-dirs :class-dir class-dir :compile-opts {:direct-linking false} :ns-compile aot-compile-nses})) (b/copy-dir {:src-dirs (into src-dirs ["resources"]) :target-dir class-dir}) (b/jar {:class-dir class-dir :jar-file jar-file}))) (defn jar-inst [_] (check-jvm) (clean nil) (println "AOT compiling inst : " aot?) (let [lib 'com.github.flow-storm/flow-storm-inst src-dirs ["src-inst" "src-shared"] basis (b/create-basis {:project nil :extra {:deps {'org.java-websocket/Java-WebSocket {:mvn/version "1.5.3"} 'com.cognitect/transit-clj {:mvn/version "1.0.333"} 'com.cognitect/transit-cljs {:mvn/version "0.8.280"} 'com.github.flow-storm/hansel {:mvn/version "0.1.90"} 'org.clojure/data.int-map {:mvn/version "1.2.1"} 'amalloy/ring-buffer {:mvn/version "1.3.1"}} :paths src-dirs}}) jar-file (format "%s/%s.jar" target-dir (name lib))] (b/write-pom {:class-dir class-dir :lib lib :version version :basis basis :src-dirs src-dirs :pom-data [[:licenses [:license [:name "Unlicense"] [:url "http://unlicense.org/"]]]]}) (when aot? (b/compile-clj {:basis basis :src-dirs src-dirs :class-dir class-dir :compile-opts {:direct-linking false} :ns-compile aot-compile-nses})) (b/copy-dir {:src-dirs src-dirs :target-dir class-dir}) (b/jar {:class-dir class-dir :jar-file jar-file}))) ================================================ FILE: deps.edn ================================================ {:paths ["src-inst" "src-dbg" "src-shared" "resources"] :deps {;; IMPORTANT !! ;; If adding any dependency for the `inst` part also add it on ;; build.clj jar-inst org.java-websocket/Java-WebSocket {:mvn/version "1.5.3"} com.cognitect/transit-clj {:mvn/version "1.0.333"} com.cognitect/transit-cljs {:mvn/version "0.8.280"} com.github.flow-storm/hansel {:mvn/version "0.1.90"} org.openjfx/javafx-controls {:mvn/version "21.0.4-ea+1"} org.openjfx/javafx-base {:mvn/version "21.0.4-ea+1"} org.openjfx/javafx-graphics {:mvn/version "21.0.4-ea+1"} org.openjfx/javafx-web {:mvn/version "21.0.4-ea+1"} org.kordamp.ikonli/ikonli-javafx {:mvn/version "12.3.1"} org.kordamp.ikonli/ikonli-materialdesign-pack {:mvn/version "12.3.1"} com.github.jpmonettas/j-system-theme-detector {:mvn/version "3.8.1"} nrepl/nrepl {:mvn/version "1.1.1"} org.clojure/data.int-map {:mvn/version "1.2.1"} org.fxmisc.richtext/richtextfx {:mvn/version "0.11.1"} amalloy/ring-buffer {:mvn/version "1.3.1"}} :aliases {:cljs-storm {:classpath-overrides {org.clojure/clojurescript nil} ;; disable the official compiler :extra-deps {thheller/shadow-cljs {:mvn/version "2.27.4" :exclusions [org.clojure/clojurescript]} ;; bring ClojureScriptStorm com.github.flow-storm/clojurescript {:mvn/version "1.11.132-2"} ;; add FlowStorm runtime dep com.github.flow-storm/flow-storm-inst {:local/root "." #_#_:mvn/version "RELEASE"} cider/cider-nrepl {:mvn/version "0.28.3"} refactor-nrepl/refactor-nrepl {:mvn/version "3.5.2"} cider/piggieback {:mvn/version "0.5.2"}} :jvm-opts ["-Dcljs.storm.instrumentOnlyPrefixes=dev" "-Dcljs.storm.instrumentEnable=true"]} :storm {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"}} :jvm-opts ["-Dflowstorm.theme=dark" "-Dclojure.storm.instrumentAutoPrefixes=false" "-Dclojure.storm.instrumentOnlyPrefixes=dev-tester" "-Dflowstorm.jarEditorCommand=emacsclient -n +<>:0 <>/<>" "-Dflowstorm.fileEditorCommand=emacsclient -n +<>:0 <>"]} :fs-timelines-counters-plugin {:extra-deps {timelines-counters/timelines-counter {:local/root "./examples/plugins/basic-plugin/"}} :jvm-opts ["-Dflowstorm.plugins.namespaces.timelines-counters=flow-storm.plugins.timelines-counters.all"]} :dev {:extra-paths ["src-dev" "classes"] :extra-deps { org.openjfx/javafx-swing {:mvn/version "21.0.4-ea+1"} ;; for scenic view to run io.github.tonsky/clj-reload {:mvn/version "0.7.1"} } :jvm-opts ["-Dvisualvm.display.name=FlowStorm" ;; for the profilers "-Djdk.attach.allowAttachSelf" "-XX:+UnlockDiagnosticVMOptions" "-XX:+DebugNonSafepoints"]} :build {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.9.6" :git/sha "8e78bcc"}} :ns-default build :jvm-opts ["-Dcljfx.skip-javafx-initialization=true"] } :test {:extra-paths ["test"] :extra-deps {lambdaisland/kaocha {:mvn/version "1.70.1086"} org.clojure/clojurescript {:mvn/version "1.11.60"}} :jvm-opts ["-Xmx10500m"] :main-opts ["-m" "kaocha.runner"]}}} ================================================ FILE: docs/_config.yml ================================================ theme: jekyll-theme-cayman ================================================ FILE: docs/dev_notes.md ================================================ # Developer notes The purpose of this document is to collect information and tips for people wanting to enhance, fix, debug, or just learn about the internals of FlowStorm. ## FlowStorm design FlowStorm is made of three parts : * An __instrumentation__ system * A __runtime__ system * A __debugger__ system If you want some diagrams of how all this works together take a look at [here](https://raw.githubusercontent.com/flow-storm/flow-storm-debugger/master/docs/high_level_diagram.svg) ### Instrumentation The __instrumentation__ system is responsible for instrumenting your code. This means interleaving extra code to trace what your program is doing. There are currently 3 ways of instrumenting : * [Hansel](https://github.com/flow-storm/hansel) a library to add instrumentation by re-writing forms at macroexpansion time * [ClojureStorm](https://github.com/flow-storm/clojure) a Clojure dev compiler that will instrument by emitting extra bytecode * [ClojureScriptStorm](https://github.com/flow-storm/clojurescript) a ClojureScript dev compiler that will instrument by emitting extra javascript They are all independent from FlowStorm but you need to choose one of them to use FlowStorm with your programs. When starting, FlowStorm will setup callbacks to them, which they will use when generating instrumentation. You can see how it hooks with each of them in : `flow-storm.tracer/[hansel-config | hook-clojure-storm | hook-clojurescript-storm]` Whatever instrumentation system the user chooses, when instrumented code runs it will hit : * `flow-storm.tracer/trace-fn-call` * `flow-storm.tracer/trace-fn-return` * `flow-storm.tracer/trace-fn-unwind` * `flow-storm.tracer/trace-expr-exec` * `flow-storm.tracer/trace-bind` See the __runtime__ for what happens next. ### Runtime The __runtime__ is FlowStorm's subsystem that runs inside the debuggee process. Its responsibility is to record all the traces that arrive via the `flow-storm.tracer/trace-*` set of functions while `flow-storm.tracer/recording` is `true`. It will record everything in two registries `flow-storm.runtime.indexes.api/forms-registry` and `flow-storm.runtime.indexes.api/flow-thread-registry`. Take a look at their docs strings for more info. The forms registry will store all the instrumented forms by form-id, which is a hash of the form. The threads registry on the other side will be storing one timeline per thread plus a multi-thread timeline when its recording is on. The timeline is the main recording structure, and what every FlowStorm functionality is build upon. You can see a diagram of the timeline internal structure [here](https://raw.githubusercontent.com/flow-storm/flow-storm-debugger/master/docs/timeline.svg). It is currently implemented as `flow-storm.runtime.indexes.timeline-index/ExecutionTimelineTree` type, which internally uses a mutable list. It would be simpler if this could be an immutable list but the decision was made because it needs to be fast to build, and without too much garbage generation, so we don't make the debuggee threads too slow. With the current architecture transients can't be used because there isn't a trace that indicates that a thread is done, so it can't be persisted. Maybe it can be done in the future if some kind of batching by time is implemented. All objects that represent traces are defined by types in `flow-storm.runtime.types.*` instead of maps. This is to reduce memory footprint. All the timelines can be explored from the repl by using the functions defined in `flow-storm.runtime.indexes.api`. The __runtime__ exposes all the indexes functionality to debuggers via `flow-storm.runtime.debuggers-api`. The main difference between this and `flow-storm.runtime.indexes.api` is that the former will return value ids instead of actual values, since not all values can leave the debuggee process (think of infinite sequences), and also because of performance, since most of the time, for big nested values, the user is interested in a small part of them and serializing is expensive. When debugging locally, the functions in `flow-storm.runtime.debuggers-api` will be called directly, while they will be called through a websocket in the remote case. The __runtime__ part is packaged into `com.github.flow-storm/flow-storm-inst` as well as in `com.github.flow-storm/flow-storm-dbg` artifacts. ### Debugger The __debugger__ is the part of FlowStorm that implements all the tools to explore the recordings with a GUI. Its main entry point is `flow-storm.debugger.main/start-debugger`. It has a bunch of subsystems that implement different aspects of it : * `flow-storm.debugger.state/state` * `flow-storm.debugger.runtime-api/rt-api` * `flow-storm.debugger.ui.main/ui` * `flow-storm.debugger.events-queue/events-queue` * `flow-storm.debugger.websocket/websocket-server` * `flow-storm.debugger.repl.core/repl` All namespaces have doc strings explaining they purpose and how they work. Not all subsystems will be running in all modes. For example the `websocket-server` and the `repl` client will be only running in remote debugging mode, since they are not needed for local debugging. The __debugger__ uses a custom component system defined in `flow-storm.state-management`, which is a very simple `mount` like component system. It doesn't use `mount` since we don't want to drag too many dependencies to the user classpath. The __debugger__ GUI is implemented as a JavaFX application, with all screens implemented in `flow-storm.debugger.ui.*`. ### The coordinate system All expressions traces will contain a `:coord` field, which specifies the coordinate inside the form with id `:form-id` a value refers to. The coordinates are stored as a string, like `"3,2"`. They are stored as strings for performance reasons. This is because on ClojureStorm, they can be compiled to the strings constant pool, which will be interned when the class is loaded and they can thereof be referenced by traces. As an example, the coordinate `"3,2"` for the form : ```clojure (defn foo [a b] (+ a b)) ``` refers to the second symbol `b` in the `(+ a b)` expression, which is under coordinate `3`. The coordinates are a zero based collection of positional indexes from the root, for all but for maps and sets. For them instead of an index it will be, for a : map key : a string K followed by the hash of the key form map value: a string V followed by the hash of the key form for the val For sets it will also be a string K followed by the hash of the set element form. As an example : (defn foo [a b] (assoc {1 10 (+ 42 43) 100} :x #{(+ 1 2) (+ 3 4) (+ 4 5) (+ 1 1 (* 2 2))})) some examples coordinates : [3 1 "K-240379483"] => (+ 42 43) [3 2 "K1305480196" 3] => (* 2 2) You can see an implementation of this hash function in `hansel.utils/clojure-form-source-hash`. The reason we can't use indices for maps and sets is because the order is not guaranteed. ### Values As described in the __runtime__ section, recorded values never leave `flow-storm.runtime.debuggers-api`. They are stored into a registry, and a reference to them is returned. This references are defined in `flow-storm.types/ValueRef`, which acts as a reified pointer. Whatever is using `flow-storm.runtime.debuggers-api` will deal with this `ValueRef`s instead of actual values. Functions are also exposed by `flow-storm.runtime.debuggers-api` to work with `ValueRef`s : * `val-pprint` for printing a value into a string representation with provided `print-level` and `print-length` The values registry implementation is a little more involved than just a map from (hash value) -> value because not all values can be hashed, specially infinite sequences. For that, every value is wrapped in a `flow-storm.runtime.values/HashableObjWrap` which will use `flow-storm.utils/object-id` for the hash. ### Events Events are a way for the __runtime__ to communicate information to the __debugger__. All possible events are defined in `flow-storm.runtime.events`. If there is nothing subscribed to runtime events the events will accumulate inside `flow-storm.runtime.events/pending-events`, and as soon as there is a subscription they will be dispatched. This is to capture events fired by recording when the __debugger__ is still not connected. On the __debugger__ side they will accumulate on `flow-storm.debugger.events-queue` and will be dispatched by a specialized thread. Most events are processed by `flow-storm.debugger.events-processor/process-event` but any part of the __debugger__ can listen to __runtime__ events by adding a callback with `flow-storm.debugger.events-queue/add-dispatch-fn`. ### Tasks Most of the __runtime__ functionality the __debugger__ calls is called synchronously, but there are some functions where doing like this leads to suboptimal UX. This are most functionality that needs to search or collect on the timeline. For big recordings this could take a long time. For this reason, functions that loop on the timeline run under tasks. This are looping process that run async, can report progress and can be interrupted. All the functions that run tasks ends up in `-task`, like `search-collect-timelines-entries-task`. From the __debugger__, for calling a task function `flow-storm.debugger.ui.tasks/submit-task` can be used. On the __runtime__ side, there are a couple of utilities that make implementing this interruptible loopings easies : - `flow-storm.runtime.debuggers-api/submit-batched-collect-interruptible-task` (for collecting functionality that traverse the entire timeline) - `flow-storm.runtime.debuggers-api/submit-find-interruptible-task` (for looping through first match) There ### Remote debugging Remote debugging means that the __debugger__ and the __runtime__ run in different processes. This is optional in Clojure but the only way of debugging in ClojureScript. The difference with local debugging is that the interaction happens over a websocket and a repl connection instead of direct function calls. Interaction here refers to api calls from the __debugger__ to the __runtime__ and events flowing the other way around. The debuggee can start a repl server which the __debugger__ can then connect to, while and the __debugger__ will start a websocket server that the __runtime__ will connect to by running `flow-storm.runtime.debuggers-api/remote-connect`. The reason there are two different kinds of connections between the same processes are capabilities and performance. The repl connection is the only way of accomplishing some functionality in ClojureScript, while the websocket is better suited for events dispatches. Most of the api calls travel trough the websocket connection, you can see which goes through which connection in `flow-storm.debugger.runtime-api/rt-api`. ## FlowStorm dev tips The easiest way in my opinion to work with the FlowStorm codebase is by starting a repl with the `:dev` and `:storm` aliases (unless one needs to specifically try Vanilla FlowStorm). The FlowStorm codebase includes a dev namespace inside `src-dev/dev.clj`, which contains some handy code for development, and is designed so you can work without having to restart the repl. Specially : * `start-local` to start everything locally, with spec checking the `flow-storm.debugger.state/state` * `start-remote` to start only the __debugger__ and connect to a different process, with spec checking the `flow-storm.debugger.state/state` * `stop` to shutdown the system gracefully * `refresh` which will use tools.namespace to refresh namespaces After the UI is running, everything you eval under the `dev-tester` namespace will be recorded. The `dev-tester` namespace contains a bunch of meaningless functions, with the only purpose of generating data for testing the system. In most situations, you should be able to change and re-eval a function on the repl and retry it without restarting the UI. ## Working with the UI When tweaking the UI [ScenicView](https://github.com/JonathanGiles/scenic-view) helps a lot, since it allows us to explore the JavaFX Nodes tree, and also reload css on the fly. ## Using FlowStorm as a backend for other tooling FlowStorm includes an nRepl middleware `flow-storm.nrepl.middleware` which exposes all its functionality as nRepl operations. For examples of tooling using this middleware, take a look at [cider-storm](https://github.com/flow-storm/cider-storm). ## Working with ClojureStorm Since ClojureStorm is a fork of Clojure, any technique you use for working with the Clojure compiler will apply here. For example if you start the repl with `:storm`, `:dev` and `:jvm-debugger` you should be able to connect a Java debugger like IntelliJ's one and add breakpoints. Then run whatever expression at the repl and step with the debugger. Another handy tool for troubleshooting instrumentation is [clj-java-decompiler](https://github.com/clojure-goes-fast/clj-java-decompiler). Let's say you have instrumented: ```clojure (defn sum [a b] (+ a b)) ``` You can then decompile it into : ```java public final class dev_tester$sum extends AFunction { public static Object invokeStatic(final Object a, final Object b) { Number add; try { Tracer.traceFnCall(new Object[] { a, b }, "user", "sum", -1340777963); Tracer.traceBind(a, "", "a"); Tracer.traceBind(b, "", "b"); Tracer.traceExpr(a, "3,1", -1340777963); Tracer.traceExpr(b, "3,2", -1340777963); add = Numbers.add(a, b); Tracer.traceExpr(add, "3", -1340777963); Tracer.traceFnReturn(add, "", -1340777963); } catch (Throwable throwable) { Tracer.traceFnUnwind(throwable, "", -1340777963); throw throwable; } return add; } @Override public Object invoke(final Object a, final Object b) { return invokeStatic(a, b); } } ``` or its bytecode equivalent. ## Working with ClojureScriptStorm For working with ClojureScript storm we can use FlowStorm! since it is a Clojure codebase. Take a look at this for ideas : https://jpmonettas.github.io/my-blog/public/compilers-with-flow-storm.html ## Installing local versions You can install both artifacts locally by running : ```bash $ VERSION=3.8.4-SNAPSHOT make install-inst # for building and installing the inst artifact $ VERSION=3.8.4-SNAPSHOT make install-dbg # for building and installing the dbg artifact ``` ================================================ FILE: docs/high_level_diagram.drawio ================================================ ================================================ FILE: docs/index.html ================================================

FlowStorm

================================================ FILE: docs/related-research-and-tools.md ================================================ - [Debugging Backwards in Time, Bil Lewis Paper](https://arxiv.org/pdf/cs/0310016.pdf) - [Debugging Backwards in Time, Bil Lewis Debugger](https://github.com/OmniscientDebugger/LewisOmniscientDebugger) - [Scalable Omniscient Debugging, Pothier, TOD, paper](https://pleiad.cl/papers/2007/pothierAl-oopsla2007.pdf) - [Scalable Omniscient Debugging, Pothier, TOD, software](https://pleiad.cl/tod/) - [NOD4J: Near-omniscient debugging tool for Java using size-limited execution trace](https://sel.ist.osaka-u.ac.jp/lab-db/betuzuri/archive/1199/1199.pdf) - [Repeatedly-Executed-Method Viewer for Efficient Visualization of Execution Paths and States in Java](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=233a50cdc9ba4ce0b322053cebbc380ec93a728c) - [JIVE: Java Interactive Visualization Environment](https://cse.buffalo.edu/jive/tutorials.html) - [Undo debugger for Java](https://undo.io/solutions/products/java/) - [ChrononSystems debugger for Java](https://chrononsystems.com/products/chronon-time-travelling-debugger) - [Wallabyjs](https://wallabyjs.com/docs/intro/time-travel-debugger.html) - [ZStep common lisp, video](https://www.youtube.com/watch?v=VzGj59hg2io) - [ZStep common lisp, paper](https://www.lirmm.fr/%7Educour/Doc-objets/ECOOP/papers/0276/02760011.pdf) ================================================ FILE: docs/run_configs.drawio ================================================ ================================================ FILE: docs/test-cases.org ================================================ * Test cases | Name | Local Clj | Remote Clj | ClojureScript | |---------------------------------+-----------+------------+----------------------| | Basic #rtrace | OK | OK | OK | | Basic #trace | OK | OK | OK | | Def val | OK | OK | OK | | Inspect val | OK | OK | OK | | REPL [un]instrument var | OK | OK | OK / ~UN-FAIL-known~ | | REPL [un]instrument namespaces | OK | OK | OK | | Correct stepping | OK | OK | OK | | Goto trace by typing idx | OK | OK | OK | | Browser navigation | OK | OK | OK | | Browser [un]instrument var | OK | OK | ~UN-FAIL-known~ | | Browser [un]instrument ns | OK | OK | OK | | Browser enable/disable instr | OK | OK | OK | | Browser [un]instr sync | OK | OK | N/A | | Cli-run | OK | N/A | N/A | | Re-run flow | OK | OK | OK | | Call tree | OK | OK | OK | | Call tree goto trace | OK | OK | OK | | Call search | OK | OK | OK | | Functions list | OK | OK | OK | | Functions list goto trace | OK | OK | OK | | Fully instrument form | OK | OK | OK | | UnInstrument forms from fn list | OK | OK | ~UN-FAIL-FAIL~ | | Taps | OK | OK | OK | | Themes | OK | OK | OK | | Auto reconnect | N/A | OK | OK | | | | | | ================================================ FILE: docs/timeline.drawio ================================================ ================================================ FILE: docs/user_guide.adoc ================================================ = FlowStorm debugger User's Guide :source-highlighter: rouge :author: By Juan Monetta :lang: en :encoding: UTF-8 :doctype: book :toc: left :toclevels: 3 :sectlinks: :sectanchors: :leveloffset: 1 :sectnums: _FlowStorm_ is a tracing debugger for Clojure and ClojureScript. image::user_guide_images/intro_screenshot.png[] It can instrument any Clojure code and provides many tools to explore and analyze your programs executions. = Quick start Before you start check _FlowStorm_ minimum requirements. [IMPORTANT] .Minimum requirements ==== - jdk >= 17 (if you still need to run it with jdk11 take a look at <<#_run_with_jdk_11,here>>) - Clojure >= 1.10.0 ==== == Clojure There are two ways of using _FlowStorm_ for Clojure : - With <<#_clojurestorm,ClojureStorm>> (recommended) : Swap your Clojure compiler at dev time by ClojureStorm and get everything instrumented automatically - <<#_vanilla_flowstorm,Vanilla FlowStorm>> : Just add FlowStorm to your dev classpath and instrument by tagging and re-evaluating forms === ClojureStorm This is the newest and simplest way of using _FlowStorm_. It requires you to swap your official Clojure compiler by _ClojureStorm_ only at dev time. Swapping compilers sounds like a lot, but don't worry, _ClojureStorm_ is just a patch applied over the official compiler with some extra stuff for automatic instrumentation. You shouldn't encounter any differences, it is only for dev, and you can swap it back and forth by starting your repl with a different alias or lein profile. The easiest way to run and learn _FlowStorm_ with _ClojureStorm_ is by running the repl tutorial. ==== Try it with no project and no config You can start a repl with FlowStorm with a single command like this : [%nowrap,bash] ---- ;; on Linux and OSX clj -Sforce -Sdeps '{:deps {} :aliases {:dev {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}}}' -A:dev ;; on Windows clj -Sforce -Sdeps '{:deps {} :aliases {:dev {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version """1.12.4"""} com.github.flow-storm/flow-storm-dbg {:mvn/version """4.5.9"""}}}}}' -A:dev ---- Pasting that command on your terminal will bring up a repl with _FlowStorm_ and the compiler swapped by _ClojureStorm_. When the repl comes up evaluate the `:dbg` keyword to bring up the UI and then click on `Help->Tutorial` on the menu for a tour of the basics. After the tutorial you may want to use it on your projects. You use it by adding a deps.edn alias or lein profile. The simplest way is to setup it globally, so that is what we are going to do next. You can also add it only to specific projects if they require special configurations. ==== Global setup as deps.edn aliases You can setup your global `~/.clojure/deps.edn` (on linux and macOS) or `%USERPROFILE%\.clojure\deps.edn` (on windows) like this : [%nowrap,clojure] ---- {... :aliases {:1.12-storm {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}} ;; Optional plugins you find yourself using regularly :fs-web-plugin {:extra-deps {com.github.flow-storm/flow-storm-web-plugin {:mvn/version "1.0.0-beta"}} :jvm-opts ["-Dclojure.storm.instrumentOnlyPrefixes.webPlugin=org.httpkit.server,ring.adapter.jetty,next.jdbc.result-set" "-Dflowstorm.plugins.namespaces.webPlugin=flow-storm.plugins.web.all"]} ...}} ---- Then you can start your repls with the `:1.12-storm` alias (like `clj -A:1.12-storm`). When the repl comes up evaluate the `:dbg` keyword to bring up the UI, then click on `Help->Tutorial` on the menu for a tour of the basics. ==== Global setup as leiningen profiles You can setup your global `~/.lein/profiles.clj` (on linux and macOS) or `%USERPROFILE%\.lein\profiles.clj` (on windows) like this : [%nowrap,clojure] ---- {:1.12-storm {:dependencies [[com.github.flow-storm/clojure "1.12.4"] [com.github.flow-storm/flow-storm-dbg "4.5.9"]] :exclusions [org.clojure/clojure]} ;; Optional plugins you find yourself using regularly :fs-web-plugin {:dependencies [[com.github.flow-storm/flow-storm-web-plugin "1.0.0-beta"]] :jvm-opts ["-Dclojure.storm.instrumentOnlyPrefixes.webPlugin=org.httpkit.server,ring.adapter.jetty,next.jdbc.result-set" "-Dflowstorm.plugins.namespaces.webPlugin=flow-storm.plugins.web.all"]} ...} ---- Then you can start your project repls with `+1.12-storm` profile (like `lein with-profile +1.12-storm repl`). When the repl comes up evaluate the `:dbg` keyword to bring up the UI, then click on `Help->Tutorial` on the menu for a tour of the basics. [NOTE] .Running lein repl without a project ==== For some reason if you run `lein with-profile +1.12-storm repl` outside of a project it will not run with the profile activated correctly. ==== ==== Per project setup with deps.edn If your project is using deps.edn, you can add an alias that looks like this : [%nowrap,clojure] ---- {... :aliases {:1.12-storm {;; for disabling the official compiler :classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}}} ---- Once you have setup your deps.edn, start your repl with the `:1.12-storm` alias and run the debugger by evaluating the `:dbg` keyworkd on your repl (this means just type `:dbg` and hit return). If it is your first time using FlowStorm, when the UI comes up click on `Help->Tutorial` on the menu for a tour of the basics. If you need more fine control over instrumentation see <<#_controlling_instrumentation,controlling instrumentation>>. ==== Setup with leiningen If your project uses lein, you can add a profile that looks like this : [%nowrap,clojure] ---- (defproject my.project "1.0.0" :profiles {:1.12-storm {:dependencies [[com.github.flow-storm/clojure "1.12.4"] [com.github.flow-storm/flow-storm-dbg "4.5.9"]] :exclusions [org.clojure/clojure]}} ...) ---- Once you have setup your lein profile globally or per project, start your repl with the `1.12-storm` profile and run the debugger by evaluating the `:dbg` keyworkd on your repl (this means just type `:dbg` and hit return). Make sure you activate the profile with `lein with-profile +1.12-storm repl`. If it is your first time using FlowStorm, when the UI comes up click on `Help->Tutorial` on the menu for a tour of the basics. If you need more fine control over instrumentation see <<#_controlling_instrumentation,controlling instrumentation>>. [NOTE] .lein dependencies ==== If you are using lein < 2.11.0 make sure your global :dependencies don't include the official org.clojure/clojure dependency. Moving to lein latest version should work ok even if your global :dependencies contains the Clojure dep. ==== === Vanilla FlowStorm If you use the https://clojure.org/guides/deps_and_cli[clojure cli] you can start a repl with the _FlowStorm_ dependency loaded like this : [,bash] ---- ;; on Linux and OSX clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' ;; on Windows clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version """4.5.9"""}}}' ---- If you are a https://leiningen.org/[lein] user add the dependency to your project.clj `:dependencies` and run `lein repl`. Then require the api namespace and start the debugger : [%nowrap,clojure] ---- user> (require '[flow-storm.api :as fs-api]) ;; the only namespace you need to require user> (fs-api/local-connect) ;; will run the debugger GUI and get everything ready ---- You should now see a empty debugger window. Click on the recording button to leave the debugger in recording mode and the let's debug something: [%nowrap,clojure] ---- user> #rtrace (reduce + (map inc (range 10))) ;; #rtrace will instrument and run some code ---- After running it, you should get the return value of the expression (as if #rtrace wasn't there), but now you will also have the debugger UI showing your recordings. From here you probably want to check out the <<#_flows_tool, Flows tool>> which contains a lot of information about exploring your recordings. == ClojureScript Debugging ClojureScript is a case of remote debugging in _FlowStorm_. This means the debugger will run in a separate process and connect to the debuggee (your browser or nodejs runtime) via a websocket and optionally an nrepl server. There are two ways of using _FlowStorm_ with ClojureScript : - With <<#_clojurescriptstorm_with_shadow_cljs,ClojureScriptStorm>> (recommended) : Swap your ClojureScript compiler by ClojureScriptStorm at dev and get everything instrumented automatically - <<#_clojurescript_vanilla_flowstorm,Vanilla FlowStorm>> : Just add FlowStorm to your dev classpath and instrument by tagging and re-evaluating forms _ClojureScriptStorm_ is a fork of the official ClojureScript compiler that adds automatic instrumentation so you don't need to think about it (you can still disable it when you don't need it). You use it by swapping the official ClojureScript compiler by _ClojureScriptStorm_ at dev time, using dev aliases or profiles. [NOTE] .Repl connection ==== For enabling every debugger feature, _FlowStorm_ needs to connect to a cljs repl. Currently only shadow-cljs repl over nrepl is supported. ==== === ClojureScriptStorm with shadow-cljs [IMPORTANT] .Minimum requirements ==== - For ClojureScript 1.11.* shadow-cljs >= 2.25.4, For ClojureScript 1.12.* shadow-cljs >= 3.1.1 - FlowStorm >= 3.7.4 ==== For setting up _FlowStorm_ with shadow-cljs you need to modify two files, your `shadow-cljs.edn` and your `deps.edn`. This is setup once and forget, so once you have configured _FlowStorm_ you can do everything from the UI, without any other sources modifications. If you want a shadow-cljs template to play with, take a look at https://github.com/jpmonettas/shadow-flow-storm-basic/[this repo]. [NOTE] .shadow-cljs ==== Currently you can only use _ClojureScriptStorm_ with shadow-cljs if you are resolving your dependencies with deps.edn. This means having `:deps true` or similar in your shadow-cljs.edn. If you have your dependencies directly in your shadow-cljs.edn you will have to use <<#_clojurescript_vanilla_flowstorm,Vanilla FlowStorm>> for now. This is because there is currently no way to swap the ClojureScript compiler in shadow-cljs.edn. ==== First, make your shadow-cljs.edn looks something like this : [%nowrap,clojure] ---- {:deps {:aliases [:1.12-cljs-storm]} :nrepl {:port 9000} ... :builds {:my-app {... :devtools {:preloads [flow-storm.storm-preload] :http-port 8021}}}} ---- So, the important parts are: you need to tell shadow to apply your deps.edn :1.12-cljs-storm alias, set up a nrepl port, and also add `flow-storm.storm-preload` to your preloads. If you have other preloads make sure `flow-storm.storm-preload` is the first one. Then, modify your `deps.edn` dev profile to look like this : [%nowrap,clojure] ---- {... :aliases ;; this alias can be defined globally in your ~/.clojure/deps.edn so you don't have to modify this file in your project {:1.12-cljs-storm {:classpath-overrides {org.clojure/clojurescript nil} ;; disable the official compiler :extra-deps {thheller/shadow-cljs {:mvn/version "3.3.4" :exclusions [org.clojure/clojurescript]} ;; bring ClojureScriptStorm com.github.flow-storm/clojurescript {:mvn/version "1.12.134-3"} ;; add FlowStorm runtime dep com.github.flow-storm/flow-storm-inst {:mvn/version "4.5.9"}}}}} ---- There are lots of things going on there, but the main ones are: disabling the official compiler, adding _ClojureScriptStorm_ and _FlowStorm_ dependencies, and then configuring what you want _ClojureScriptStorm_ to automatically instrument. By default the JVM property `cljs.storm.instrumentAutoPrefixes` is true so all your project top level namespaces will be instrumented automatically. If you need to set that property to false it is important to configure what namespaces you want to instrument, and you do this by setting the `cljs.storm.instrumentOnlyPrefixes` jvm property. This is a comma separated list of namespaces prefixes, you normally want your app namespaces plus some libraries, like : `cljs.storm.instrumentOnlyPrefixes=org.my-app,org.my-lib,hiccup` And this is it. Once you have it configured, run your shadow watch as you normally do, load your app on the browser (or nodejs). Whenever your need the debugger, on a terminal run the ui with your shadow-cljs.edn data : [,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-app ---- and then reload you page so it connects to it. Since we started the app with `flowstorm.startRecording=false` you will have to click on the record button once to start recording. Whenever recording is enable and something executes under an instrumented namespace you should see the recordings appear in the debugger under the main thread. [NOTE] .recording expressions typed on the repl ==== If you type at the repl something like `(defn foo [a b] (+ a b))` under an instrumented ns, the `foo` function will get instrumented automatically and you will able to explore the recordings after the function is called. On the other side, typing a simple expression like `(+ 1 2)` will not show anything, this is currently a limitation but you can still make that work by wrapping the expression on a fn and immediately calling it, like `((fn [] (+ 1 2)))` ==== === ClojureScriptStorm with cljs.main You can use _FlowStorm_ and _ClojureScriptStorm_ with cljs.main. The easiest way to try it is just by starting a repl, like this : [%nowrap,bash] ---- clj -Sforce -J-Dcljs.storm.instrumentOnlyPrefixes=cljs.user -Sdeps '{:deps {com.github.flow-storm/clojurescript {:mvn/version "1.12.134-3"} com.github.flow-storm/flow-storm-inst {:mvn/version "4.5.9"}}}' -M -m cljs.main -co '{:preloads [flow-storm.storm-preload]}' --repl ---- If you run the command above you are running cljs.main --repl which will start a ClojureScript repl on your terminal and open a browser connected to it. You runtime will also start with FlowStorm preloaded and everything under `cljs.user` is going to be instrumented. Then on a different terminal run the _FlowStorm_ UI : [%nowrap,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger ---- And now refresh your browser page so your browser app connects to the UI. [NOTE] .Limitations ==== There are some small limitations like not being able to modify instrumentation from the UI without restarting the repl. This is because the _FlowStorm_ UI needs to also connect via nrepl to JVM process running the compiler, which isn't available when running cljs.main. ==== === ClojureScript vanilla FlowStorm Let's say you are using https://github.com/thheller/shadow-cljs[shadow-cljs] to start a ClojureScript repl. First you need to add _FlowStorm_ dependency to your project dependencies, like this : [%nowrap,clojure] ---- $ cat shadow-cljs.edn {... :dependencies [... [com.github.flow-storm/flow-storm-inst "4.5.9"]] ;; the next two lines aren't needed but pretty convenient :nrepl {:port 9000} :my-build-id {:devtools {:preloads [flow-storm.preload]}} ...} ---- Then let's say you start your repl like : [,bash] ---- npx shadow-cljs watch :my-build-id shadow-cljs - config: /home/jmonetta/demo/shadow-cljs.edn shadow-cljs - server version: 2.19.0 running at http://localhost:9630 shadow-cljs - nREPL server started on port 9000 shadow-cljs - watching build :my-build-id [:my-build-id] Configuring build. [:my-build-id] Compiling ... [:my-build-id] Build completed. (127 files, 0 compiled, 0 warnings, 6.19s) cljs.user=> ---- As you can see from the output log shadow-cljs started a nrepl server on port 9000, this is the port _FlowStorm_ needs to connect to, so to start the debugger and connect to it you run : [,bash] ---- ;; on linux and mac-os clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-build-id ;; on windows clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version """4.5.9"""}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-build-id ---- And that is all you need, the debugger GUI will pop up and everything will be ready. Try tracing some code from the repl : [%nowrap,clojure] ---- cljs.user> #rtrace (reduce + (map inc (range 10))) ;; #rtrace will instrument and run some code ---- After running it, you should get the return value of the expression (as if #rtrace wasn't there). The debugger thread list (the one on the left) shows all the threads it has recordings for. Because we are in javascript land there will always be just one thread, called `main`. Double clicking it should open the "thread exploring tools" for that thread in a new tab. This guide will cover all the tools in more detail but if you are interested in code stepping for example you will find it in the `code stepping tool` at the bottom left corner of the thread tab, the one that has the `()` icon. Click on it and use the stepping controls to step over the code. Now that everything seems to be working move on and explore the many features _FlowStorm_ provides. There are many ways of instrumenting your code, and many ways to explore its executions. If you are not using a repl or the repl you are using isn't supported by _FlowStorm_ yet you can still use the debugger but not all features will be supported (mainly the browser features). For this you can start the debugger like before but without any parameters, like this : [,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger ---- And then go to your app code and call `(flow-storm.runtime.debuggers-api/remote-connect)` maybe on your main, so every time your program starts will automatically connect to the repl. [NOTE] .ClojureScript environments ==== _FlowStorm_ is supported for ClojureScript in : - Browsers - NodeJS - React native ==== [NOTE] .NodeJs and react-native ==== On NodeJs and react-native you need to install the `websocket` library. Do this by running `npm install websocket --save` For react-native if your app is running inside a cellphone you will have to also provide the `:debugger-host` key to `flow-storm.debugger.main/start-debugger` with your box ip address, unless you are using adb reverse with your ports for which you will have to `adb reverse tcp:7722 tcp:7722` (the debugger websocket port) ==== [NOTE] .App initialization debugging ==== If you need to debug some app initialization, for adding `#trace` tags before the debugger is connected you will have to require flow-storm.api yourself, probably in your main. All the tracing will be replayed to the debugger once it is connected. ==== Here is a repo you can use if you want to try _FlowStorm_ with shadow-cljs https://github.com/flow-storm/shadow-flow-storm-basic === Multiple ClojureScript builds You can setup FlowStorm to debug multiple ClojureScript builds. This can be useful when your application is made up of multiple parts, like when you have web workers. Debugging multiple builds require multiple debugger instances, one per build. The FlowStorm UI will start a websocket server, by default on 7722, so if you want to run multiple instances of it, you need to run each instance under a different port. You can do this by providing a `:ws-port` to the startup command. So let's say you want to run two debuggers, one for your page and one for a webworker, your can run them like this : [,bash] ---- # on one terminal start your app debugger instance clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-app :ws-port 7722 # on a second terminal start your webworker debugger instance clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-web-worker :ws-port 7733 ---- Now you also need to configure your builds to tell them what port they should connect to. You do this by writing different preloads for each of your builds, and then using them instead of your `flow-storm.storm-preload`, like: `my_app.main_storm_preload.cljs` [%nowrap,clojure] ---- (ns my-app.main-storm-preload (:require [cljs.storm.tracer] [flow-storm.tracer :as tracer] [flow-storm.runtime.debuggers-api :as dbg-api])) (dbg-api/start-runtime) (tracer/hook-clojurescript-storm) (dbg-api/remote-connect {:debugger-host "localhost" :debugger-ws-port 7722}) ---- `my_app.webworker_storm_preload.cljs` [%nowrap,clojure] ---- (ns my-app.webworker-storm-preload (:require [cljs.storm.tracer] [flow-storm.tracer :as tracer] [flow-storm.runtime.debuggers-api :as dbg-api])) (dbg-api/start-runtime) (tracer/hook-clojurescript-storm) (dbg-api/remote-connect {:debugger-host "localhost" :debugger-ws-port 7733}) ---- They are the same as `flow-storm.storm-preload` just with different port numbers. Now you can configure your shadow-cljs.edn like this : [%nowrap,clojure] ---- {... :builds {:app {:target :browser ... :modules {:my-app {:init-fn my.app/init :preloads [my-app.main-storm-preload]} :my-webworker {:init-fn my.app.worker/init :preloads [my-app.webworker-storm-preload] :web-worker true}}}}} ---- [NOTE] .Multiple debuggers tips ==== You can change the theme or customize the styles of different instances to make it easier to know which debugger instance is connected to which application. ==== == Babashka You can debug your babashka scripts with FlowStorm using the JVM. The process is quite simple. Let's say we want to debug this example script https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj which runs a webserver with a basic todo app. First we need to generate a deps.edn by running `bb print-deps > deps.edn` Then modify the resulting deps.edn to add the FlowStorm alias like this : [%nowrap,clojure] ---- {... :aliases {:dev {:classpath-overrides {org.clojure/clojure nil} ;; for disabling the official compiler :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}} :jvm-opts ["-Dclojure.storm.instrumentOnlyPrefixes=user"]}}} ---- With `clojure.storm.instrumentOnlyPrefixes=user` we are telling ClojureStorm to instrument everything inside the `user` namespace since the script doesn't contain any namespace declaration. And that is it, you can now start your clojure repl as usual, with `clj -A:dev` and then eval the `:dbg` keyword to start the debugger UI. Then eval the entire file to compile everything. To start the server in this example you will have to remove the wrapping that is basically only allowing the server to run if we are running from babashka, like this : [%nowrap,clojure] ---- (when true #_(= *file* (System/getProperty "babashka.file")) ...) ---- so we can also start it from Clojure. After the server has started, you can use the app from the browser and everything will get recorded as usual. = The tool bar The toolbar as well as the menu provides quick access to some general commands. image::user_guide_images/toolbar.png[] From left to right: - Cancel current running task. Whenever you a running a task that can take some time, this button will be red, and you can use it to cancel the task. - The `Inst enable` button allows to enable/disable instrumentation when in a Storm environment. A change on instrumentation will only affect newly compiled code. = Flows tool The `Flows` vertical tab contains a bunch of tools for recording and analyzing your programs executions. First of all, what are Flows? A Flow is an "execution flow" recording unit. The only purpose of a flow is to group recording activity. This grouping allows us for example to run some code and record it under `flow-0`, then modify our code, run it again, and record this second run (or flow) under `flow-1`. Now we can access both recordings separately. image::user_guide_images/recording_controls.png[] When you first open FlowStorm UI you will see four things, from left to right : - Clear your recordings if any. - Start/Stop recording. You can keep your heap from growing by stopping recording when you don't need it. - Start/Stop recording the multi-thread timeline. Check out the <<#_multi_thread_timeline, multi-thread timeline>> tool. - The `Rec on` combo-box to select under what flow new recordings are going to be stored. Whenever there is something recorded for a flow, a new tab with the flow name will appear. Execution inside a flow will be grouped by threads. So the first thing you will see on a flow is a menu of threads we have recordings for so far. This threads will be referred sometimes as timelines, since they are a sequence of recorded execution steps. Let's say for example we have selected to record under `flow-1` and run some multi threaded code. We are going to see something like this : image::user_guide_images/multi_flows_1.png[] There is a lot going on in the screenshot above, but the most important are : - we have configured FlowStorm to record new executions under `flow-1` - we have recorded stuff under `flow-1` and there are also some previous recordings under `flow-0` - we are currently looking at `flow-1`, we have opened to explore the thread with id `1` called `main` and we are exploring it in <<#_code_stepping,the code stepper>> - `Threads [4]` indicates we have recorded activity in 4 threads, which we can access via this menu Now for a different example : image::user_guide_images/multi_flows_2.png[] This second image shows us exploring the recordings of a thread with id `474`, called `pool-4-thread-4` on `flow-0`. image::user_guide_images/flows_toolbar.png[] The `Flows tool` also contains a toolbar that contains the Quick jump box. Use it for quickly opening the first recording of a function in <<#_code_stepping,the code stepper>>. Will autocomplete the first 25 matches. In the screenshot above we see analyzing the recordings in <<#_code_stepping,the code stepper>> but there are many tools to explore the recorded timelines, which we are going to describe next. == Code tool image::user_guide_images/code_tool_tab.png[] The code tool is the first of the `Flows` tab. It provides most of the functionality found in a traditional debugger. You can use it to step over each expression, visualize values, locals and more. === Code stepping The code tool allows you to step and "travel throught time" in two ways: - Use the controls at the top to step over your code in different ways. - Click on the highlighted forms to position the debugger at that point in time. image::user_guide_images/controls.png[] For moving around using the controls we have two rows of buttons. The second row of controls, the most important one, are the stepping controls. From left to right they are : - Step over backwards, will make one step backwards always staying on the same frame. - Step backwards, will step backwards in time going into sub functions. - Step out, will position the debugger in the next step after this function was called. - Step forward, will step forward in time going into sub functions. - Step over forward, will make one step forwards always staying on the same frame. The numbers at the center show `current_step_index / total_steps`. This means that a total of `total_steps` has been recorded for this thread so far. Write any number (less than total_steps) on the text box to jump into that position in time. The buttons around the step counter are : - Jump to the first step of the recording. - Jump to the last step of the recording. On the first row we have more controls, also for moving around in time. From left to right we have : - Undo navigation - Redo navigation - Add a <<#_bookmarks, bookmark>> - The last stepping controls to the right are the <<#_power_stepping, power stepping>> controls. [NOTE] .Highlighting ==== Only the forms that were executed at least once for the current function frame will be highlighted. ==== This means that code can be un-highlighted for two reasons: - there isn't any recording for that part of the code - there is a recording but doesn't belong to this function frame. image::user_guide_images/stepper_highlighting.png[] In the contrived example above we see we are stepping the `foo` function. All inside this function body is highlighted but the bodies of the two anonymous functions for mapping and reducing. This will only get highlighted once you step into their bodies. In this case you are sure there are recordings for these functions bodies because the reduce is non lazy, so if you keep stepping eventually you will get into their bodies, but there is a faster way. image::user_guide_images/stepper_highlighting_2.png[] For this you can right click on any un-highlighted expression that you think there could be a recording for and select `Jump forward here`. image::user_guide_images/stepper_highlighting_3.png[] This will make FlowStorm scan from the current point of the timeline searching forward for a value recorded at that coordinate (if any) and move the stepper to that point in time. You also have `Jump to first record here` which will scan from the beginning of the timeline and `Jump backwards here` which will search backwards from the current position. === Power stepping image::user_guide_images/controls_power_custom.png[] The controls at the right are power stepping controls. They provide more powerfull ways of stepping through the code. Clicking on the first, back, next or last buttons will navigate the timeline using the selected power stepping tool in the dropdown. There are currently six power stepping tools : - `identity`, will step to the prev/next value which identity is the same as the current value. - 'equality', will step to the prev/next value which is equals (clojure equality) to the current value. - `same-coord` will step to the prev/next value for the same coordinate. This means it will move to the next recording in the timeline for this exact place in the code you are currently in. You can also see it as take me to all the situations when the current expression executed doesn't matter how we got to it. - `custom`, allows you to provide a predicate, which will be used to find the next step. If you define it like `(fn [v] (map? v))` will make the power stepper step over all map values. - `custom-same-coord`, the same as `custom` but fixed on the current coordinate like `same-coord`. - `identity-other-thread`, will step to a position which identity is the same as the current value in a different thread. Here the prev and next arrows do the same thing, it will just jump to the first position that matches this value on a different thread. This has some limitations. If there are more than two threads working with this identity there is no way of choosing which thread to go. If you need more control, checkout the <<#_programmable_debugging,programmable debugging>> section, specially the `find-expr-entry` function. - `fn-call`, allows you to provide a function to step to. [NOTE] .Custom stepping ==== Custom power stepping is only supported in Clojure now. ==== Power stepping automatically skips all values equals to `:flow-storm.power-step/skip`. This can be useful when combined with <<#_dealing_with_mutable_values, snapshot-value>> as a way of ignoring some of them, which provides a way of sampling tight loops like in games. === Searching image::user_guide_images/search_access.png[] You can use the search tool to search over all your flow recorded expressions and then make the stepper jump to them. You can find the search tool under `More tools -> Search`. There are multiple ways of searching: - By pr-str - By data window current value - By predicate ==== Searching by pr-str image::user_guide_images/search_pr_str.png[] This type of search will walk over the selected threads expressions, converting their values to strings with `pr-str` up to the selected level and depth and then checking if the resulting string contains your provided query string. ==== Searching by DataWindow value image::user_guide_images/search_data_window.png[] Searching by data window value allows you to select any of the current data windows and will search for the current selected data window value over the selected threads expressions values using identity. ==== Searching by predicate image::user_guide_images/search_pred.png[] Searching by predicate allows you to provide a Clojure predicate which will be used over all selected threads expressions values. === Loops Whenever you click a highlighted form that has been executed multiple times inside the same function call (any kind of loop), instead of immediately jumping into it, FlowStorm will popup a menu, like in the picture below : image::user_guide_images/loops.png[] This is the loops navigation menu. It allows you to quickly move around interesting iterations of the loop. The menu will display slightly different options depending on you current position. The `[FIRST] ...` and `[LAST] ...` entries will always show, which allows you to quickly jump to the first and last iteration of the loop. If you are currently before the loop, clicking into any expression inside the loop will show the first 20 values for the clicked expression. If instead you are currently in a expression after the loop, clicking back to an expression inside the loop, will show the last 20 values for the clicked expression. Now if you are currently stepping inside the loop, clicking any other expression inside it will show you 10 values before and 10 values after of your current position. Clicking on any of this entries will take you to that position in time. If this is not enough, and you want to see all the values taken by some expression along the loop, you can always use the <<#_printer, printer tool>>. === Exceptions debugging `FlowStorm` will capture all functions that didn't return because an exception unwind the stack, even when that exception was captured further and it didn't bubble up. image::user_guide_images/exceptions.png[] When an unwind situation is recorded a combobox will show up in the toolbar, containing the functions names together with the exceptions types. If you hover the mouse over any of them, a tooltip will display the exception message. Clicking on any of them will position the stepper at that point in time so you can explore what happened before. You can configure FlowStorm to automatically jump to exceptions with the `Config` menu by checking `Auto jump to exception` which is disabled by default. === Locals The locals panel will show the locals visible for the current point in time and their values at binding time. image::user_guide_images/locals.png[] Right clicking on them will show a menu where you can : - define all - define the value with a name, so you can use it at the repl - inspect the value with a <<#_data_windows,data window>> - tap the value as with `tap>` `Define all` will define all the bindings currently visible in the locals pane in the current form namespace. This is useful for trying things at your editor as described here https://www.cognitect.com/blog/2017/6/5/repl-debugging-no-stacktrace-required [NOTE] .Locals and mutable values ==== The Locals pane will show the value of each binding for a symbol at binding time, which are the same thing no matter where you are in the current block when working with immutable objects, but not when working with mutable ones. If what was bound was muttable in any way, you will be seeing the value at binding time, and not at current time. ==== === Stack The stack panel will always show the current stacktrace. Be aware that the stacktrace only include functions calls that had been recorded, so if you aren't recording everything there will be gaps. image::user_guide_images/stack.png[] Double clicking on any of the stack entries will make the debugger jump to that point in time. === Value panels Value panels show in many places in _FlowStorm_. image::user_guide_images/value_panels2.png[] The value panel in the code tool always display a pretty print of the current expression value. You can configure the print-level and print-meta for the pretty printing by using the controls at the top. The value panel showing the current expression in the code stepper is a little bit special since it also contains a <<#_data_windows,data window>> tab which allows you to quickly navigate the value or give it custom visualizations. image::user_guide_images/value_panels1.png[] ==== Define value for repl Use the `def` button to define a var pointing to the current inspector value. You can use / to provide a namespace, otherwise will be defined under [cljs.]user === Goto to file:line Clicking on the `Actions->Goto file:line` menu allows you to search and jump to the first recording of a expression with a file and line, given that one exists. It will ask you for a file and line in the format of `:`. If you have a file like `src/org/my_app/core.clj` and you are interested in expressions evaluating on like 42 you should search like `org/my_app/core.clj:42`. == Call Stack tree tool The call stack tree tool is the second one of the `Flows` tab. It allows you to see the execution flow by expanding its call stack tree. image::user_guide_images/callstack_tool_tab.png[] The call stack tree is useful for a high level overview of a complex execution and also as a tool for quickly moving through time. You can jump to any point in time by double clicking on a node or by right clicking and on the context menu selecting `Step code`. image::user_guide_images/callstack_tree.png[] Use the button at the top left corner of the tree tool to show the current frame of the debugger in the tree. There are also two <<#_value_panels,value panels>> at the bottom that show the arguments and return value for the currently selected function call. [NOTE] .Disabling the call stack tree tool ==== The call stack tree tool can be enable/disable on the fly if you are not using it and performance is an issue, since keeping it updated can be expensive. You can disable it from the Config menu or via the `flowstorm.callTreeUpdate=false` JVM prop. ==== == Functions tool The functions tool is the third one of the `Flows` tab. image::user_guide_images/functions_tool_tab.png[] It shows a list of all traced functions sort by how many times the have been called. image::user_guide_images/functions_calls.png[] Normal functions will be colored black, multimethods magenta and types/records protocols/interfaces implementations in green. Together with the <<#_call_stack_tree_tool, call stack tree>> it provides a high level overview of a thread execution, and allows you to jump through time much faster than single stepping. You can search over the functions list by using the bar at the top. === Function calls Clicking on the calls counter of any function will display all function calls on the right sorted by time. Each line will show the arguments vector for each call, and their return value. Use the check boxes at the top to hide some of the arguments. image::user_guide_images/function_calls.png[] Double clicking on any row in the functions call list will jump to the stepper at that point in time. You can also use the `args` and `ret` buttons to open the values on the inspector. == Multi-thread timeline You can use this tool to record, display and navigate a total order of your recordings in a timeline. This can be used, for example, to visualize how multiple threads expressions interleave, which is sometimes useful to debug race conditions. You enable/disable the multi-thread timeline recording using its button on the toolbar. Recording on the multi-thread timeline will make your program execution a little slower so it is recommended to have it paused unless you need it. When you have something recorded on the multi-thread timeline you access the tool from the top right corner. image::user_guide_images/multi_timeline_access.png[] As an example, let's say you record the execution this function : [,clojure] ---- (defn run-parallel [] (->> (range 4) (pmap (fn [i] (factorial i))) (reduce +))) ---- By opening the tool a window like this should pop up : image::user_guide_images/timeline.png[] As you can see the timeline tool displays a linear representation of your expressions. Times flows from top to bottom and each thread gets assigned a different color. Every time a function is called or returns you will see it under the `Function` column, and for each expression executed you will see a row with its `Expression` and `Value`. Double clicking any row will make your code stepper (on the main window) jump to the code at that point in time. [NOTE] .Big recordings timeline ==== Rendering the timeline needs some processing to render each sub-form and print each value so be aware it could be slow if you try it on big recordings. ==== There is also a `Only functions?` checkbox at the top that will retrieve only function calls and can be used to visualize the threads interleaving at a higher level. == Printer _FlowStorm_ has a lot of functionality to replace printing to the console as a debugging method since most of the time it is pretty inefficient. Nonetheless, sometimes adding a bunch of print lines to specific places in your code is a very powerful way of understanding execution. For this cases _FlowStorm_ has the `Printer tool`, which allows you to define, manage and visualize print points, without the need of re running your code. It will work on your recordings as everything else. You can add and re run print points over your recordings as many times as you need. To add a print point, just right click on any recorded expression. image::user_guide_images/printer_add.png[] It will ask you for a couple optional fields. image::user_guide_images/printer_add_box.png[] The `Message format` is the "println text". A message to identify the print on the printer output. Here you can use any text, in which you can optionally use `%s` for the printed value, same as you would use it with format. The `Expression` field can be use to apply a transformer function over the value before printing it. Useful when you want to see a part of the value. image::user_guide_images/printer_access.png[] After you add them, you can access the `Printers tool` by navigating to `More tools -> Printers`. The threads selector allows you to select the thread the prints are going to run on. Leaving it blank will run prints over all threads recordings (checkout the notes for caveats). Clicking the `refresh` button will [re]run the printing again over the current recordings. image::user_guide_images/printer.png[] You can tweak your prints at any time, like changing the print-length, print-level, message, transform-fn or just temporarily disable any of them. When you are ok re-setting you prints, just click refresh and they will print again. Double clicking on any printed line will jump to the Flows code tab, with the debugger pointed to the expression that generated the print. [IMPORTANT] .Multi-thread prints order ==== If you select `All` threads, and have a multi-thread timeline recording, then the printer will use it and you can use prints to debug threads interleaving for example, but if you run your printers with `All` threads selected without a multi-thread timeline recording they will print sorted by thread and not in the order they happened. ==== == Bookmarks Bookmarks are another quick way of jumping around in code and they can be added from your code or the FlowStorm UI. You can find you bookmarks on the top menu `View -> Bookmarks`. image::user_guide_images/bookmarks.png[] Double clicking on any bookmark will make the debugger jump back to its position. === Code bookmarks You add code bookmarks by adding the `(bookmark)` statement to your code, which optionally accepts a label. The first time a bookmark statement is executed it will make the FlowStorm UI jump to it. Since this behavior is similar to a `debugger` statement in languages like Javascript, it is also aliased as `(debugger)` so you can use whichever you prefer. [NOTE] .ClojureScript support ==== This is currently only supported when using ClojureScriptStorm >= 1.11.132-9 ==== === UI bookmarks UI bookmarks are useful when you find yourself jumping around, trying to understand a complex execution. They enable you to mark execution positions so you can come back to them later. image::user_guide_images/bookmarks_add_btn.png[] You can bookmark the current position by pressing the bookmark button in the code tool, next to your stepping controls. It will ask you the bookmark description. = Browser tool The browser tool is pretty straight forward. It allows you to navigate your namespaces and vars, and provides ways of <<#_controlling_instrumentation,managing what gets instrumented>>. image::user_guide_images/browser.png[] = Outputs tool image::user_guide_images/outputs.png[] The outputs tool can be used instead of your normal IDE/Editor panel to visualize your evaluations results, your taps outputs and your `*out*` and `*err*` streams writes (like printlns). The advantages being : - Custom visualizations - Quick nested values navigation - Quick taps values navigation - Datafy nav navigation - Access to all previously tapped values - Access to the last 10 evaluated values (instead of just `*1` and `*2`) - Ability to search tapped values in Flows The taps visualization system works out of the box while the evals result and printing capture currently depends on you using nrepl and starting with the flow-storm middleware. Checkout the outputs setup section for instructions. [NOTE] .ClojureScript support ==== Only the taps viewer is currently supported on ClojureScript. The last evaluations and the out and err streams capture aren't supported yet. ==== == Middleware setup For using all the features in the Outputs tool you need to be using nrepl and start your repl with `flow-storm.nrepl.middleware/wrap-flow-storm` middleware. If you use Cider for example you can add it to `cider-jack-in-nrepl-middlewares` via customizing the global value or by using `.dir-locals.el`. == Output data window The top panel is a <<#_data_windows,data window>> for displaying evaluations and taps. As soon as you evaluate or tap something it will be displayed here. == Last evals The last evals pane gives you access to the last 10 evaluation results, same as `*1` and `*2`. Click on any value to display it on the top data window. == Taps Everytime _FlowStorm_ starts, it will add a tap, so whenever you `tap>` something it will show on the taps list. Click on any value to display it on the top data window. If the tapped value has also been recorded as an expression in Flows, you can right click on it and run `Search value on Flows` to move the debugger to that point in time. [NOTE] .Search value on Flows ==== Be aware that if the code that taps your value is something like `(tap> :a-key)` you won't be able to jump to it using this, because `:a-key` isn't a value recorded by _FlowStorm_, while if the tapping code is like `(tap> some-bind)` or `(tap> (+ 2 3))` or the tapping of any other expression you should be able to jump to it. So if you want to use this functionality as a "mark" so you can quickly jump to different parts of the recordings from the Taps tool, you can do it like `(tap> (str :my-mark))` ==== A `#tap` tag will also be available, which will tap and return so you can use it like `(+ 1 2 #tap (* 3 4))` Use the `clear` button to clear the list. There is also `#tap-stack-trace`. It will tap the current stack trace. == Out and Err streams Everything written on `*out*` or `*err*` will be captured and displayed on the bottom panel. You can copy anything from this area with normal tools. = Data Windows image::user_guide_images/data_window.png[] Data Windows are a user extensible tool to visualize and explore your data. Their role is to support : - a way to navigate nested structures in a lazy way - visualize and navigate metadata - multiple visualizations for each value - lazy/infinite sequences navigation - a way to define the current sub-values so you can use them at the repl - a mechanism for realtime data visualization - clojure.datafy navigation out of the box - tools for the user to add custom visualizations on the fly The next sections will explore each of them. == Data navigation image::user_guide_images/data_window_dig.png[] You can navigate into any key or value by clicking on it. Use the breadcrumbs at the top to navigate back. == Metadata navigation image::user_guide_images/data_window_meta.png[] If any value contains metadata, it will be shown at the top. Clicking on it will make the data window navigate into it. == Multiple visualizers image::user_guide_images/data_window_multiple_viz.png[] You can change how to display your current value by using the visualizers selector dropdown at the top. == Sequences image::user_guide_images/data_window_seqable.png[] The seqable visualizer allows you to navigate all kind of sequences (even infinite ones) by bringing more pages on demand. Click on `More` to bring the next page in. == Defining values You can always define a var for the current value being shown on any data window by clicking the `def` button. Clicking on it will raise a popup asking for a symbol name. If you don't provide a fully qualified symbol it will define the var under `user` or `cljs.user` if you are in ClojureScript. A quick way to use it is to provide a short name, let's say `foo`, and then access it from your code like `user/foo`. == Realtime visualizations image::user_guide_images/data_window_realtime.png[] DataWindows not only support displaying and navigating values, but also updating them in real time from your application. From your program's code you can always create a data window with : [,clojure] ---- (flow-storm.api/data-window-push-val :changing-long-dw-id 0 "a-long") ---- by providing a data window id, a value, and optionally the initial breadcrumb label. But you can also update it (given that the selected visualizer supports updating like :scope for numbers) with : [,clojure] ---- (flow-storm.api/data-window-val-update :changing-long-dw-id 0.5) ---- This `data-window-val-update` is pretty useful when called from loops or refs watches, specially paired with a custom visualizer. == Clojure datafy/nav image::user_guide_images/data_window_datafy_nav.png[] Data Windows support datafy nav out of the box. The data window will always be showing the result of `clojure.datafy/datafy` of a value. For maps or vectors where keys provide navigation it will automatically add a blue arrow next to the value. Clicking on the value will just dig the data, while clicking on the blue arrow will navigate as with `clojure.datafy/nav` applied to that collection on that key. == EQL pprint visualizer image::user_guide_images/eql_visualizer_0.png[] image::user_guide_images/eql_visualizer_1.png[] The `eql-query-pprint` visualizer allows you to explore your data "entities" by looking at subsets of it using queries similar to datomic pull queries like in the screenshots above. [NOTE] .Disable by default ==== The EQL query pprint is disable by default. To enable it call `(flow-storm.runtime.values/register-eql-query-pprint-extractor)`. ==== By entities it means maps which contains only keywords as their keys. Every other collection is just traversed. This are all valid queries : - `[*]` - `[:name]` - `[:name :age :vehicles]` - `[:name :age {:vehicles [:type]}]` - `[:name :age {:vehicles [?]}]` - `[:name {:vehicles [*]}]` - `[:name :age {:vehicles [:type {:seats [?]}]}]` - `[:name :age {:vehicles [:type {:seats [:kind]}]}]` - `[:name {:houses [:rooms]}]` The `*` symbol means include all keys, while the `?` symbol means just list the keys, which helps exploring big nested maps with many keys. == Custom visualizers An important aspect of Data Windows is to be able to provide custom visualizers on the fly. Let's say we have model a chess board as a set of maps which represent our pieces. [,clojure] ---- (def chess-board #{{:kind :king :player :white :pos [0 5]} {:kind :rook :player :white :pos [5 1]} {:kind :pawn :player :white :pos [5 3]} {:kind :king :player :black :pos [7 2]} {:kind :pawn :player :black :pos [6 6]} {:kind :queen :player :black :pos [3 1]}}) (flow-storm.api/data-window-push-val :chess-board-dw chess-board "chess-board") ---- If we open a data window with `data-window-push-val` we are going to see something like this : image::user_guide_images/data_window_custom1.png[] but we can do better, we can create a custom visualizer so we can see it like this : image::user_guide_images/data_window_custom2.png[] Data visualization in FlowStorm is composed of two things: - a data aspect extractor, which runs on the runtime process, and will build data for the visualization part - a visualizer, which runs on the debugger process, and will render extracted data for a value using javafx For a basic Clojure session everything will be running under the same process, but this is not the case for ClojureScript or remote Clojure. First let's require some namespaces : [,clojure] ---- (require '[flow-storm.api :as fsa]) (require '[flow-storm.debugger.ui.data-windows.visualizers :as viz]) (require '[flow-storm.runtime.values :as fs-values]) ---- We can register a custom visualizer by calling `register-visualizer`. [,clojure] ---- (viz/register-visualizer {:id :my-viz :pred (fn [val] ) :on-create (fn [val] {:fx/node :any-java-fx-node-that-renders-the-value :more-ctx-data :anything}) ;; OPTIONALLY :on-update (fn [val created-ctx-map {:keys [new-val]}] ) :on-destroy (fn [created-ctx-map] ) }) ---- The important part there are `:id`, `:pred`, and `:on-create`. The `:id` will be the one displayed on the visualizers dropdown, and re-registering a visualizer with the same id will replace the previous one. `:pred` is a predicate on the data extracted from values, it should return true if this visualizer can handle the value. And `:on-create` will be a function that receives this value and renders a java fx node. The val passed to on-create will also contain two special keywords : - :flow-storm.debugger.ui.data-windows.data-windows/dw-id The id of the data windows it's being draw on - :flow-storm.debugger.ui.data-windows.data-windows/preferred-size (could be :small) Optionally you can provide `:on-update` and `:on-destroy`. `:on-update` will receive values from the runtime via `fsa/data-window-val-update`. It will also get a handle to the original value (the one that created the DataWindow) and whatever map was returned by `:on-create`. `:on-destroy` will be called everytime a visualizer gets removed, because you swapped your current visualizer or because you went back with breadcrums. It can be useful in case you need to clear resources created by `:on-create`. `:pred` and `:on-create` will not receive the original value but the extracted aspects of it after all registered extractors run. You can check the data available to your visualizer for a value in a data window by calling : [,clojure] ---- (viz/data-window-current-val :chess-board-dw) ---- If the data already extracted from your value is not enough for your visualizer you can register another extractor. === Data aspect extraction [,clojure] ---- (fs-values/register-data-aspect-extractor {:id :chess-board :pred (fn [val _] (and (set? val) (let [{:keys [kind player pos]} (first val)] (and kind player pos)))) :extractor (fn [board _] {:chess/board board})}) ---- In this case we are going to register and extractor that will only run for vals which are sets and contains at least one element which is a map with `:kind`, `:player` and `:pos`. The extracted data will be the entire board. All ids of extractors that applied for a value will be appended under `::fs-values/kinds` of the value as you will see next. === Visualizers Now we can register a visualizer that will show only on values which contains a :chess-board kind. [,clojure] ---- (import '[javafx.scene.layout GridPane]) (import '[javafx.scene.control Label]) (viz/register-visualizer {:id :chess-board ;; only be available if the chess-board data extractor run on this value :pred (fn [val] (contains? (::fs-values/kinds val) :chess-board)) ;; use the chess/board info to render a board with java fx :on-create (fn [{:keys [chess/board]}] (let [kind->sprite {:king "♚" :queen "♛" :rook "♜" :bishop "♝" :knight "♞" :pawn "♟"} pos->piece (->> board (mapv #(vector (:pos %) %)) (into {}))] {:fx/node (let [gp (GridPane.)] (doall (for [row (range 8) col (range 8)] (let [cell-color (if (zero? (mod (+ col (mod row 2)) 2)) "#f0d9b5" "#b58863") {:keys [kind player]} (pos->piece [row col]) cell-str (kind->sprite kind "") player-color (when player (name player))] (.add gp (doto (Label. cell-str) (.setStyle (format "-fx-background-color:%s; -fx-font-size:40; -fx-text-fill:%s; -fx-alignment: center;" cell-color player-color)) (.setPrefWidth 50)) (int col) (int row))))) gp)}))}) ---- === ClojureScript Using custom visualizers with ClojureScript (or other remote environments) is a little bit more involved. Registering aspect extractors is exaclty the same, since they run on the runtime (browswer, node, etc), but custom visualizers should be registered on the debugger process. For this you need to create your visualizers in some namespace, let's say on `/dev/visualizers.clj`, add the `dev` folder to your classpath and then running the debugger UI with something like : [,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-app :pre-require visualizers ---- Notice the last option `:pre-require visualizers`. This will allow you to load the just defined `visualizers` namespace before starting the UI. == Default visualizers You can make any visualizer the default by calling `add-default-visualizer` which takes a predicate on the val-data (the same received by :on-create) and a visualizer key, like this : [,clojure] ---- (viz/add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :chess-board)) :chess-board) ---- For all FlowStorm provided visualizers take a look at `flow-storm.debugger.ui.data-windows.visualizers` namespace. Default visualizers predicates are added in a stack, and tried from the top. This means that you can always overwrite a default by adding a new one. = Thread breakpoints image::user_guide_images/thread_breaks.png[] _FlowStorm_ is a tracing debugger, which means it can record what is happening without the need of stopping your programs execution. This is all fine but doesn't cover every possible situation. There are cases where recording everything is impractical, and even if you can start/stop recording whenever you want, being able to automatically stop your threads at certain points is useful. For these cases, _FlowStorm_ has the ability to set thread breakpoints, which means to define points (functions) in the execution of your program where you want your threads to wait. While the threads are waiting you can explore what happened so far. As soon as a thread hits a break function, if recording is on, it will be blocked, and a "Threads blocked" menu will show up in the UI. You can use this menu to unblock different threads. Once you are done, you can pause recording using the pause button in the main toolbar and un-block every thread. You can define thread breakpoints in two ways : - Using the browser (like in the image below), you can navigate to any function and click on the `Break` button. This will block the calling thread every time the selected function gets called. - Or you can also install a break by calling (flow-storm.api/break-at 'my-proj.core/some-fn) image::user_guide_images/browser_breakpoints.png[] [NOTE] .Conditional threads breakpoints ==== The break-at fn accepts a second argument where you can provide a predicate that will be called with the same arguments of the function you are breaking. It will only break when the predicate returns true. If you don't provide a predicate it will default to `(constantly true)` ==== You can remove breakpoints by : - Clicking on the browser instrumentation list delete buttons - Calling `flow-storm.api/remove-break` to remove a single breakpoint - Calling `flow-storm.api/clear-breaks` to remove all breakpoints = Programmable debugging _FlowStorm_ gives you full access to its internal indexes from the repl in Clojure and ClojureScript. These allows you to explore your recordings using Clojure and write small programs to analyze them if what's provided by the GUI is not enough. Most of what is documented here is also documented in the `flow-storm.runtime.indexes.api` namespace docstring, which you can retrieve by evaluating `(doc flow-storm.runtime.indexes.api)`. In fact, this is the only namespace you need to require on your repl in order to work with your recordings. Let's say you have recorded some execution and now you want to work with the recordings from the repl. So first we require the api ns as `ia`. [,clojure] ---- (require '[flow-storm.runtime.indexes.api :as ia]) ---- Now from the UI, you can get the thread-id of your recordings (the number next to the thread name) which you will need for accessing them from the repl. == Timelines Let's say you want to explore recordings on thread 32. You can retrieve its timeline by calling `ia/get-timeline` like this : [,clojure] ---- (def timeline (ia/get-timeline 32)) ---- Once you have the timeline you can start exploring it. The timeline implements many of the Clojure basic interfaces, so you can : [,clojure] ---- user> (count timeline) 798 user> (take 3 timeline) ; (#flow-storm/fn-call-trace [Idx: 0 org.my-app/run-server] ; #flow-storm/fn-call-trace [Idx: 1 org.my-app/read-config] ; #flow-storm/fn-call-trace [Idx: 2 org.my-app/check-config]) user> (get timeline 0) ; #flow-storm/fn-call-trace [Idx: 0 org.my-app/run-server] ---- The easiest way to take a look at a thread timeline is with some code like this : [,clojure] ---- (->> timeline (take 3) (map ia/as-immutable)) ; ({:type :fn-call, ; :fn-ns "org.my-app", ; :fn-name "run-server", ; :ret-idx 797, ; :fn-call-idx 0, ; :parent-idx nil, ; :fn-args [], ; :form-id -798068730, ; :idx 0} ; ... ; ...) ---- In most cases converting all entries into maps with `ia/as-immutable` is enough, but if you want a little bit more performance you can access entries information without creating a immutable map first. Timelines entries are of 4 different kinds: `FnCallTrace`, `FnReturnTrace`, `FnUnwindTrace` and `ExprTrace`. You can access their data by using the following functions depending on the entry : All kinds : - `as-immutable` - `fn-call-idx` `ExprTrace`, `FnReturnTrace` and `FnUnwindTrace` : - `get-coord-vec` `ExprTrace`, `FnReturnTrace` : - `get-expr-val` `FnUnwindTrace` : - `get-throwable` `FnCallTrace` : - `get-fn-name` - `get-fn-ns` - `get-fn-args` - `get-fn-parent-idx` - `get-fn-ret-idx` - `get-fn-bindings` You can also access the timeline as a tree by calling : - `callstack-root-node` - `callstack-node-childs` - `callstack-node-frame-data` Take a look at their docstrings for more info. == Forms You can retrieve forms by form id with `get-form` and then use `get-sub-form-at-coord` and a coordinate. Here is a little example : [%nowrap,clojure] ---- ;; retrieve some expression entry into expr user> (def expr (-> timeline (get 3) ia/as-immutable)) user> expr {:type :expr, :coord [2 2 1], :result 4, :fn-call-idx 2, :idx 3} ;; retrieve the fn-call entry for our expr user> (def fn-call (-> timeline (get (:fn-call-idx expr)) ia/as-immutable)) user> fn-call {:type :fn-call, :fn-ns "dev-tester" :fn-name "other-function", :form-id 1451539897, ...} ;; grab it's form user> (def form (-> fn-call :form-id ia/get-form :form/form)) user> form (def other-function (fn [a b] (+ a b 10))) ;; lets look at the sub-form from form at our expr coordinate user> (ia/get-sub-form-at-coord form (:coord expr)) a ---- == Multi-thread timeline If you have recorded a multi-thread timeline, you can retrieve it with `total-order-timeline` like this : [,clojure] ---- (def mt-timeline (ia/total-order-timeline)) ---- which you can then iterate using normal Clojure functions (map, filter, reduce, get, etc). The easiest way to explore it is again with some code like this : [,clojure] ---- user> (->> mt-timeline (take 3) (map ia/as-immutable)) ({:thread-id 32, :type :fn-call, :fn-call-idx 0, :fn-ns "org.my-app", :fn-name "run", :fn-args [], :ret-idx 797, :parent-idx nil, :form-id -798068730, :idx 0} ... ...) ---- Notice that each of these entries contains a flow-id and thread-id also. == Other utilities There are other utitities in the api ns that could be useful, some of the most interesting ones : - `find-expr-entry` useful for searching expressions and return values with different criteria. - `find-fn-call-entry` useful for searching functions calls with different criteria. - `stack-for-frame` - `fn-call-stats` Take a look at their docstrings for more info. = LLM agents You can teach a LLM how to use FlowStorm's api to help you analyze your recordings. If you are using the amazing https://github.com/bhauman/clojure-mcp[clojure-mcp] you just need to upload https://github.com/flow-storm/flow-storm-debugger/blob/master/llm-prompt.txt[one more file] that teaches the LLM FlowStorm's basics from the repl. https://claude.ai/share/489c9124-b1a8-4a33-b50a-52e4f3d4709f[Here] is a very basic chat asking Claude to look at some recordings of a buggy TODO's web application. = Remote debugging You can remotely debug any Clojure application that exposes a nrepl server. In terms of dependencies, the debuggee side should be setup the same as a normal local setup, with the optional change that you can use `flow-storm-inst` instead of `flow-storm-dbg`, being the former a slimmed down version of the later one that doesn't contain some libraries used only by the UI, but using the full `flow-storm-dbg` is also ok. == SSH tunnel The easiest way to debug a remote application is via a ssh tunnel. You can create it from your dev box like this : [,bash] ---- ssh -L 9000:localhost:9000 -R 7722:localhost:7722 my-debuggee-box.com ---- assuming your remote process at my-debuggee-box.com has started a nrepl server listening on port 9000 and that the debugger websocket server is running on the default port. After the tunnel is established, you can run you debugger UI like this : [,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 ---- and that is it. If you need to connect the debugger to a remote process without a ssh tunnel or you need to configure the websocket server port you can do it like this : [,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port NREPL-PORT :runtime-host '"YOUR-APP-BOX-IP-ADDRESS"' :debugger-host '"YOUR-BOX-IP-ADDRESS"' :ws-port WS-SERVER-PORT ---- == Out of process Sometimes you are not debugging across a network but you want to run the FlowStorm UI on a different process. A couple of aliases that can help for this : [,clojure] ---- {:aliases ;; for your system process {:runtime-storm {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-inst {:mvn/version "4.5.9"}}} ;; for the FlowStorm GUI process :ui-storm {:extra-deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}} :exec-fn flow-storm.debugger.main/start-debugger :exec-args {:port 7888}}}} ;; set your nrepl port here! ---- With those aliases you can start your application process by adding `:runtime-storm` and that is it. To start the FlowStorm UI, go to another terminal and run `clj -X:ui-storm`. == Docker If you run you process inside a docker container, here is a basic template for using _FlowStorm_ with it https://github.com/jpmonettas/docker-flow-storm-basic = Dealing with too many traces When recording an application's execution (specially when using _ClojureStorm_ or _ClojureScriptStorm_) it could happen that your process starts running out of heap. This section documents some tools FlowStorm provides to deal with this situations. Most of the time, having the recording paused and just enabling it right before executing the action you are interested in is enough, but when it isn't, here are some other options. == Fn call limits A common situation is to see some high frequency functions adding a lot of noise to your recordings. For example a MouseMove event processing will generate a lot of recordings while you use your app. There are a couple of ways to limit your functions calls by thread. You can identify this kind of functions with the <<#_functions_tool,functions tool>>. One tool you can use in this situations is the `flowstorm.threadFnCallLimits` JVM prop. For example, you can add `"-Dflowstorm.threadFnCallLimits=org.my-app/fn1:2,org.my-app/fn2:4"` so every time the system starts, limits will be set for `org.my-app/fn1` and `org.my-app/fn2`. The number next to them is the limit. When a function reaches the limit _FlowStorm_ will stop recording calls to it and all the functions down its callstack. You can also modify the limits from your repl, by calling `flow-storm.runtime.indexes.api/[add-fn-call-limit|rm-fn-call-limit|get-fn-call-limits]`. In ClojureScript you need to call them via your cljs repl api. These limits are per thread, so when a thread recording is created it will start with the current defined counters, and each time a function gets called the counter will decrement. When it reaches zero the function and all functions calls under it will stop being recorded. When you clear your threads you are also clearing its limit counters, so next time you record something new counters will be initialized from your global limits definitions. == Trace and heap limits If you are tracing some code that ends up in a infinite loop the debugger will choke on too many traces, making everything slow and your only option is probably to restart it. For preventing this, _FlowStorm_ provides a couple of fuse/breakers, on threads trace count and on heap limits. They are off by default but you can enable it from the Config menu. === Trace limits Let's say you added a thread trace limit of 1000. If you now run any code where a thread generates more than a 1000 traces FlowStorm will only record those first 1000 traces and then discard the rest as if recording is off for that thread, while it will keep recording threads that haven't reached the limit. Your code will continue execution as normal, which you can break using your normal editor breaking commands if its an infinite loop, but now you have recordings to look at what is going on. You can set a limit of 0 to disable it again. You can set this limits at startup via the JVM options `"-Dflowstorm.threadTraceLimit=1000"` and `"-Dflowstorm.throwOnLimit=true"`. === Heap limits Another option is to automatically stop recording when a certain heap limit in megabytes is reached. This can also be set at startup via the JVM option `"-Dflowstorm.heapLimit=1000"`, which means stop recording as soon as we used 1000Mb of heap space. = Dealing with mutable values _FlowStorm_ will retain all values pointers when code executes so you can analyze them later. This works great with immutable values but when your code uses mutable values like this : [,clojure] ---- (let [a (java.util.ArrayList.)] (count a) (.add a "hello") (count a) (.add a "world") (.add a "!")) ---- then every time you step over `a` it will contain the last value ["hello" "world" "!"]. You can fix this situation by extending the flow-storm.runtime.values/SnapshotP protocol like this : [,clojure] ---- (extend-protocol flow-storm.runtime.values/SnapshotP java.util.ArrayList (snapshot-value [a] (into [] a))) ---- to provide _FlowStorm_ a way of creating a snapshot of the mutable value. [NOTE] .ClojureStorm ==== If you are using _ClojureStorm_ evaluate the previous defmethod in a ns that is not being instrumented to avoid an infinite recursion. ==== Be aware that this is tricky in multithreading situations, as always with mutable values. [NOTE] .Automatic derefing ==== FlowStorm will automatically deref Atoms, Refs, Agents, Vars and all pending-realized derefables on tracing so no need to implement `flow-storm.runtime.values/snapshot-value` for them. ==== [NOTE] .Snapshoting and nested values ==== Snapshoting only applies to direct references to mutable values. For example if you have an atom inside a nested immutable collection, it will not be snapshoted every time that collection expression is being recorded, because the value being recorded is not a reference to an atom. If this is important to you, you can still define snapshot-value for clojure.lang.PersistentArrayMap, etc, and walk it down snapshoting everything mutable inside. ==== [NOTE] .snapshot-value and memory footprint ==== Although snapshot-value was created as a way to deal with mutable values it can be used to replace any value by another in the recordings, which can be useful in other situations like reducing memory footprint when you don't need the entire value to be recorded. ==== = Controlling instrumentation If you are using _ClojureStorm_ or _ClojureScriptStorm_ it is important to learn how to control what gets instrumented and how to uninstrument things. You can configure what gets instrumented automatically on startup via JVM properties but also change this while your repl is running without the need to restart it. FlowStorm by default will automatically figure out what to instrument from your project, which you can always disable by setting the `-Dclojure.storm.instrumentAutoPrefixes=false`. [NOTE] .How are auto prefixes calculated? ==== When the process starts it will scan all source folders on the classpath (everything not inside a jar containing clojure files) and build a set of all top level namespace. All namespaces under those will be instrumented. Currently it doesn't detect single level namespaces, like when you have `src/core.clj`, if this is your case use instrumentOnlyPrefixes. ==== If you prefer to be explicit about what gets instrumented you can use the JVM property `"-Dclojure.storm.instrumentOnlyPrefixes=YOUR_INSTRUMENTATION_STRING"` where `YOUR_INSTRUMENTATION_STRING` should be a comma separated list of namespaces prefixes like : my-project.,lib1.,lib2.core which means automatically instrument my-project.* (which includes all sub namespaces), all lib1.* and only everything under lib2.core All this can be changed after without restarting your repl from <<#_modifying_instrumentation_with_the_browser, FlowStorm browser>>. == Turning instrumentation on an off You can turn instrumentation on an off by using the button on <<#_the_tool_bar,the toolbar>>. Remember that the change of this setting will only be effective on newly compiled code. == Setup startup instrumentation The first important thing is to setup your instrumentation correctly via JVM properties : On _ClojureStorm_ : [,clojure] ---- -Dclojure.storm.instrumentOnlyPrefixes=my-app,my-lib -Dclojure.storm.instrumentSkipPrefixes=my-app.too-heavy,my-lib.uninteresting -Dclojure.storm.instrumentSkipRegex=.*test.* ---- On _ClojureScriptStorm_ : [,clojure] ---- -Dcljs.storm.instrumentOnlyPrefixes=my-app,my-lib -Dcljs.storm.instrumentSkipPrefixes=my-app.too-heavy,my-lib.uninteresting ---- Apart from `instrumentOnlyPrefixes` which you probably already know, there is `instrumentSkipPrefixes` which also accepts a comma separated list of namespaces prefixes to skip, and instrumentSkipRegex with accepts a regex for namespaces to skip. All these together allows you to instrument you whole app but some undesired namespaces. The next important thing is to be able to enable/disable instrumentation and add/remove prefixes without restarting the repl. == Modifying instrumentation with the Browser You can use the `Browser tool` to check and change on the fly the prefixes you configured in the previous section. image::user_guide_images/browser_storm_instrumentation_1.png[] Right clicking any namespace will give you options for what level of a namespace you want to instrument. On the bottom pane (instrumentations) you will see your current instrumentation configuration (if any). Here we can see that everything under `ring.middleware.anti-forgery` will be instrumented every time something inside it gets compiled. You can remove entries using the `del` buttons or temporarily disable/enable them using the `Enable all` checkbox. image::user_guide_images/browser_storm_instrumentation_2.png[] You can use the `Add` menu in the picure above to add instrumentation prefixes. After changing any prefix FlowStorm will ask if you want it to reload the affected namespaces for you. Namespace reloading will all reload all namespaces it depends on in topological order, so it shouldn't break your system in any way. image::user_guide_images/browser_storm_instrumentation_3.png[] You can also provide functions to be called before and after reloading in case you need to stop and start your system with : [,clojure] ---- (flow-storm.api/set-before-reload-callback! (fn [] (println "Before reloading"))) (flow-storm.api/set-after-reload-callback! (fn [] (println "After reloading"))) ---- [NOTE] .Instrumentation ==== Just changing the prefixes without reloading will not make your currently loaded code [un]instrumented. If you haven't let FlowStorm reload them for you, you can always recompile them as usual with your editor commands or by executing something like `(require 'the-selected.namespace :reload)`. ==== == Instrumentation in Vanilla FlowStorm [NOTE] .ClojureStorm ==== Instructions here only apply to vanilla _FlowStorm_. If you are using _ClojureStorm_ or _ClojureScriptStorm_ (recommended) this is done automatically for you, so just skip this section. ==== Code instrumentation in _FlowStorm_ is done by rewriting your code, in a way that doesn't change its behavior but when executed will trace everything the code is doing. === Instrument any form with #trace You can instrument any top level form at the repl by writing `#trace` before it, like this : [,clojure] ---- #trace (defn sum [a b] (+ a b)) ---- and then evaluating the form. important:: `#trace` is meant to be used with forms that don't run immediately, like: defn, defmethod, extend-type, etc. Use `#rtrace` to trace and run a form, like `#rtrace (map inc (range 10))`. === Run code with #rtrace `#rtrace` is useful in two situations : First, when instrumenting and running a simple form at the repl, like: [,clojure] ---- #rtrace (-> (range) (filter odd?) (take 10) (reduce +)) ---- === Instrument namespaces _FlowStorm_ allows you to instrument entire namespaces by providing `flow-storm.api/instrument-namespaces-clj`. You call it like this : [,clojure] ---- (instrument-namespaces-clj #{"org.my-app.core" "cljs."}) ---- The first argument is a set of namespaces prefixes to instrument. In the previous example it means instrument all namespaces starting with `org.my-app.core`, and all starting with `cljs.` The second argument can be a map supporting the following options : - `:excluding-ns` a set of strings with namespaces that should be excluded - `:disable` a set containing any of #{`:expr` `:binding` `:anonymous-fn`} useful for disabling unnecessary traces in code that generate too many - `:verbose?` when true show more logging === What can't be instrumented? These are some limitations when instrumenting forms : 1. Very big forms can't be fully instrumented. The JVM spec has a limit on the size of methods and instrumentation adds a lot of code. When instrumenting entire namespaces, if you hit this limit on a form a warning will printed on the console saying `Instrumented expression is too large for the Clojure compiler` and _FlowStorm_ automatically tries to instrument it with a lighter profile, by disabling some instrumentation. 2. Functions that call recur without a loop 3. Functions that return recursive lazy sequences. Like `(fn foo [] (lazy-seq (... (foo))))` === Un-instrument code Un-instrumenting code that has been instrumented with `#trace` or `#ctrace` is straight forward, just remove the tag and re evaluate the form. To un-instrument entire namespaces you can use `flow-storm.api/uninstrument-namespaces-clj` which accept a set of namespaces prefixes. === Instrument with the browser Most of the time you can instrument code by just clicking in the browser. The only exceptions are functions that were just defined in the repl and weren't loaded from a file. ==== Instrument vars Using the browser you can navigate to the var you are interested in and then use the instrument button to instrument it. image::user_guide_images/browser_var_instrumentation.png[] There are two ways of instrumenting a var : - Instrument (instrument just the var source code) - Instrument recursively (recursively instrument the var and all vars referred by it) ==== Instrument namespaces Using the browser you can also instrument multiple namespaces. Do this by selecting the namespaces you are interested in and then a right click should show you a menu with two instrumentation commands. image::user_guide_images/browser_ns_instrumentation.png[] - `Instrument namespace :light` - record function arguments and return values (not expressions, no bindings tracing) - `Instrument namespace :full` fully instrument everything Light instrumentation is useful when you know the functions generate too many traces, so you can opt to trace just functions calls and returns. You can then <<#_fully_instrument_a_form_from_the_code_view, fully instrument>> whatever functions you are interested in. ==== Un-instrument code The bottom panel shows all instrumented vars and namespaces. image::user_guide_images/browser_uninstrument.png[] You can un-instrument them temporarily with the enable/disable checkbox or permanently with the del button. ==== Fully instrument a form from the code view image::user_guide_images/fully_instrument_form.png[] If you have instrumented a form with the <<#_instrument_namespaces_2, :light profile>> you can fully instrument it by right clicking on the current form and then clicking `Fully instrument this form`. = Plugins FlowStorm plugins allows you to add specialized tools to visualize and interact with your recordings. == Using plugins For using a pluggin follow each plugging instructions which should normally consists of adding its dependency and then setting the jvm prop `flowstorm.plugins.namespaces` with all the main namespaces of the plugins you want loaded at startup, like : `"-Dflowstorm.plugins.namespaces=flow-storm.plugins.my-plugin.all"` After that you should see a new vertical tab with the plugin UI as you can see here : image::user_guide_images/plugin_demo.png[] == Creating plugins Creating a pluging consists of two parts : - The runtime code that will analyze the recordings and expose an api for the UI - The UI component which will visualize and interact with the data via the runtime api This split is not required, but it is important if you want your plugin to support ClojureScript also or remote Clojure debugging where the UI is not running in the same process as the runtime. This components are normally split in two files, a runtime.clj and ui.clj, but you can name them however you want. We are going to go over each part in more detail but for a real plugin please checkout the https://github.com/flow-storm/flow-storm-async-flow-plugin[core.async.flow plugin]. === Runtime Here is a runtime file template you can use : [%nowrap,clojure] ---- (ns flow-storm.plugins.my-plugin.runtime (:require [flow-storm.runtime.indexes.api :as ia] [flow-storm.runtime.debuggers-api :as dbg-api])) (defn my-data-extraction [flow-id thread-id] (let [timeline (ia/get-timeline flow-id thread-id)] (reduce (fn [acc tl-entry] ;; extract some interesting info from the timeline ) {} timeline) )) ;; Expose your function so it can be called from the UI part (dbg-api/register-api-function :plugins.my-plugin/extract-data my-data-extraction) ---- === UI Here is a ui file template you can use : [%nowrap,clojure] ---- (ns flow-storm.plugins.my-plugin.ui (:require [flow-storm.debugger.ui.plugins :as fs-plugins] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]]) (:import [javafx.scene.control Label])) (fs-plugins/register-plugin :my-plugin {:label "My plugin" :css-resource "flow-storm-my-plugin/dark.css" :dark-css-resource "flow-storm-my-plugin/dark.css" :light-css-resource "flow-storm-my-plugin/light.css" :on-focus (fn [{:keys [some-other-data]}] ;; This gets called everytime the plugin tab gets focused ) :on-create (fn [_] {:fx/node (Label. ;; You can call your runtime registered function (str (runtime-api/call-by-fn-key rt-api :plugins.my-plugin/extract-data [0 10]))) :some-other-data 42}) :on-flow-clear (fn [flow-id {:keys [some-other-data]}] ;; this gets called everytime a flow is discarded so you can update your plugin UI accordignly ) }) ---- ==== Styling plugins UIs As you saw in the ui plugin registration, you can provide three resources related to styling : - :css-resource If there is any, it will be loaded and applied. Here is where you put your JavaFX pluging styles - :dark-css-resource This styles are going to be applied only in dark mode - :light-css-resource This styles are going to be applied only in light mode For making sure you plugin styles doesn't mix with other styles, your plugin is automatically wrapped in a pane with your plugin key (my-plugin in the example above) as a class. This means your plugin css can contain code like : [%nowrap,clojure] ---- .my-plugin .table-view { -fx-font-family: 'monospaced'; } ---- == List of known plugins - https://github.com/flow-storm/flow-storm-web-plugin - https://github.com/flow-storm/flow-storm-flowbook-plugin - https://github.com/flow-storm/flow-storm-cljs-compiler-plugin - https://github.com/flow-storm/flow-storm-async-flow-plugin = JVM options list This section only collects the options, search for them in the User's guide for more context and possible values. == Clojure and ClojureScript - `-Dflowstorm.startRecording=false` - `-Dflowstorm.plugins.namespaces[.+]=ns1,ns2` - `-Dflowstorm.threadFnCallLimits=org.my-app/fn1:2,org.my-app/fn2:4` - `-Dflowstorm.title=FlowStormMainDebugger` - `-Dflowstorm.theme=dark` - `-Dflowstorm.styles=~/.flow-storm/big-fonts.css` - `-Dflowstorm.fileEditorCommand=emacsclient -n +\<>:0 \<>` - `-Dflowstorm.jarEditorCommand=emacsclient -n +\<>:0 \<>/\<>` - `-Dflowstorm.threadTraceLimit=1000` - `-Dflowstorm.throwOnLimit=true` - `-Dflowstorm.autoUpdateUI=false` - `-Dflowstorm.callTreeUpdate=false` - `-Dflowstorm.uiTimeoutMillis=4000` == Only Clojure - `-Dclojure.storm.instrumentEnable=true` - `-Dclojure.storm.instrumentOnlyPrefixes[.*]=ns-prefix1,ns-prefix2` - `-Dclojure.storm.instrumentAutoPrefixes=false` - `-Dclojure.storm.instrumentSkipPrefixes[.*]=my-app.too-heavy,my-lib.uninteresting` - `-Dclojure.storm.instrumentSkipRegex=.\*test.*` - `-Dflowstorm.heapLimit=1000` == Only ClojureScript - `-Dcljs.storm.instrumentEnable=true` - `-Dcljs.storm.instrumentOnlyPrefixes=ns-prefix1,ns-prefix2` - `-Dcljs.storm.instrumentAutoPrefixes=false` - `-Dcljs.storm.instrumentOnlyPrefixes=my-app,my-lib` - `-Dcljs.storm.instrumentSkipPrefixes=my-app.too-heavy,my-lib.uninteresting` = Styling and theming All functions that start the debugger ui (`flow-storm.api/local-connect`, `flow-storm.debugger.main/start-debugger`) accept a map with the `:styles`, `:title` and `:theme` keywords. If `:styles` points to a css file it will be used to overwrite the default styles, in case you want to change colors, make your fonts bigger, etc. `:theme` could be one of `:auto` (default), `:light`, `:dark`. Title can be used to distinguish between multiple debugger instances. Like this : [,clojure] ---- user> (local-connect {:styles "~/.flow-storm/big-fonts.css", :theme :dark, :title "FlowStormMainDebugger"}) ---- If you are using _ClojureStorm_ you can also provide them with : -Dflowstorm.title=FlowStormMainDebugger -Dflowstorm.theme=dark -Dflowstorm.styles=~/.flow-storm/big-fonts.css You can overwrite all the styles defined here https://github.com/flow-storm/flow-storm-debugger/blob/master/resources/flowstorm/styles/styles.css = Controlling logging FlowStorm uses JUL (java.util.logging) as the loggging library. You can configure JUL logging by starting your repl with `-Djava.util.logging.config.file=./logging.properties` If you need to disable logging you can put this in your `logging.properties` file : [,text] ---- handlers = java.util.logging.ConsoleHandler flow_storm.level = SEVERE clojure.storm.level = SEVERE ---- = Key bindings == General - `Ctrl-g` Cancel any long running task (only search supported yet) - `Ctrl-l` Clean all debugger state - `Ctrl-d` Toggle debug-mode. Will log useful debugging information to the console - `Ctrl-u` Unblock all breakpoint blocked threads if any - `Ctrl-t` Rotate themes - `Ctrl-plus` Increment font size - `Ctrl-minus` Decrement font size - `F` "Select the Flows tool" - `B` "Select the Browser tool" - `T` "Select the Taps tool" - `D` "Select the Docs tool" == Flows - `0-9` Open focus flow-N threads menu, N being the pressed key - `t` Select the tree tool (needs to be inside a thread) - `c` Select the code tool (needs to be inside a thread) - `f` Select the functions tool (needs to be inside a thread) - `P` Step prev over. Go to previous step on the same frame - `p` Step prev - `n` Step next - `N` Step next over. Go to next step on the same frame - `^` Step out - `<` Step first - `>` Step last - `Ctrl-f` Copy current function symbol - `Ctrl-Shift-f` Copy current function call form - `Ctrl-z` Undo navigation - `Ctrl-r` Redo navigation = Debugging react native applications Debugging ClojureScript react native application needs a combination of ClojureScript and remote debugging. Assuming you are using shadow-cljs, have added the `flow-storm-inst` dependency, and that it started a nrepl server on port 9000, you can start a debugger and connect to it by running : [,bash] ---- clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :your-app-build-id :debugger-host '"YOUR_DEV_MACHINE_IP"' ---- You also need to make it possible for the device to connect back to the debugger on port 7722. You can accomplish this by running : [,bash] ---- adb reverse tcp:7722 tcp:7722 ---- Also remember that you need to have installed the `websocket` npm library. You can do this like : [,bash] ---- npm install websocket --save ---- = Working on Windows with WSL2 For those using current versions of WSL2 on Windows it should be pretty straight forward. - export DISPLAY=:0 - export WSL2_GUI_APPS_ENABLED=1 Font issues had been reported on some distros, like `java.lang.NullPointerException: Cannot read field "firstFont" because "" is null` which seams to be solved just by installing font packages like `dejavu-fonts` or `ttf-dejavu` depending on the distro. = Opening forms in editors You can add this two jvm options to tell FlowStorm how to open forms in files and inside jars : - flowstorm.jarEditorCommand : a command with optional \<>, \<> and \<> placeholders - flowstorm.fileEditorCommand : a command with optional \<> and \<> placeholders If you define those, clicking on your forms namespaces link in the code tool should run the provided commands. On expressions sub-forms that contains line meta you should also be able to right click and select "Open in editor" which should open the file at that specific line (useful for long forms). Here are some known setups for most common editors : == Emacs [,clojure] ---- ;; for opening your project files "-Dflowstorm.fileEditorCommand=emacsclient -n +<>:0 <>" ;; simple way for opening files inside jars (works on linux only) "-Dflowstorm.jarEditorCommand=emacsclient -n +<>:0 <>/<>" ;; for opening files inside jars that works on every OS (requires FlowStorm >= 3.17.3) "-Dflowstorm.jarEditorCommand=emacsclient --eval '(let ((b (cider-find-file \"jar:file:<>!/<>\"))) (with-current-buffer b (switch-to-buffer b) (goto-char (point-min)) (forward-line (1- <>))))'" ---- == VSCode [,clojure] ---- "-Dflowstorm.fileEditorCommand=code --goto <>:<>" ---- == IntelliJ [,clojure] ---- "-Dflowstorm.fileEditorCommand=idea --line <> <>" ---- == Vim [,clojure] ---- "-Dflowstorm.fileEditorCommand=vim +<> <>" ---- = Editors/IDEs integration == Emacs Checkout https://github.com/flow-storm/cider-storm[Cider Storm] an Emacs Cider front-end with support for Clojure and ClojureScript. == VSCode With the following alias setup in deps.edn: [source,clojure] {:aliases {:flowstorm {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}} :jvm-opts ["-Dflowstorm.startRecording=true" "-Dclojure.storm.intrumentEnable=true" "-Dclojure.storm.intrumentAutoPrefixes=true"]}}} Create a custom connect sequence in the VSCode settings.json: [source,json] { "name": "flowstorm", "projectType": "deps.edn", "cljsType": "none", "extraNReplMiddleware": ["flow-storm.nrepl.middleware/wrap-flow-storm"], "afterCLJReplJackInCode": "((requiring-resolve 'flow-storm.storm-api/start-debugger))", "menuSelections": { "cljAliases": ["flowstorm"] } } Jack-in using the `flowstorm` sequence from the menu. == IntelliJ IDEA (Cursive) = Tutorials and demos https://github.com/flow-storm/flow-storm-debugger?tab=readme-ov-file#some-demo-videos-newers-at-the-top = Known limitations and issues == Code with macros that don't preserve meta FlowStorm works fine with most macros, except the ones that don't preserve meta at macroexpansion, which FlowStorm needs in their absence, can cause GAPS IN EXECUTION TRACING AND PREVENT IT FROM LINKING THE EXECUTED CODE BACK TO THE ORIGINAL SOURCE. When macros are involved, the forms compiled by the Clojure compiler aren't the ones on your source files, but the ones generated by macro-expanding all the macros. In order to link the compiled forms back to the forms in your source code, for each instrumented form, right after the form is read by the reader, FlowStorm will walk the form down, annotating with meta each sub-form with a coordinate, which will then be used after macro expansion to link a compiled expression back to your source code. Macros can be as simple as code reorganizing ones (like `->`, `when`, `and`, `defn`, etc) or whole compilers like `clojure.core.async/go` and Electric, so it really depends on the macros. If you see code inside a macro not being traced feel free to report an issue, there is nothing FlowStorm can do from its side but we can work together with the macro developer making sure it preserves all meta after macro expansion, which sometimes may be possible. == Locals pane and mutable values The Locals pane will show the value of each binding for a symbol AT BINDING TIME, which is the same thing no matter where you are in the current block when working with immutable objects, BUT NOT WHEN WORKING WITH MUTABLE ONES. If what was bound was mutable in any way, you will be seeing the value at binding time, and not at current time which could cause some confusion. == Closed over bindings aren't displayed in Locals The locals pane will only display bindings for the current function. Locals visible from the current function but not defined in it (like in the case of closures) aren't shown. == Don't instrument clojure.core or FlowStorm itself Currently we can't instrument clojure.core or FlowStorm itself since they endup in infinite tracing recursions. This can be solved, but it is currently a limitation. == IF test expressions tracing with intrinsics When you have code like : [,clojure] ---- (defn foo [^long l] (if (zero? l) (+ l 1) (+ l 2))) ---- because `l` is a primitive long, the compiler can replace the (zero? l) with intrinsics (LCONST_0, LCMP, IFNE) so the (zero? l) isn't a expression anymore, just a statement. In these cases you will see the if test return un-highlighted, but you can still tell which branch the code went thru because the chosen branch will be the highlighted one. == Loading an entire file while recording records some weird code This is because most editors, (specially via nrepl) eval some other code on your namespace in order to load the contents of your file. If that namespace is instrumented this will be also recorded, even when it is probably not of your interest. This is harmless, just clear your recordings before running and recording anything else. If you follow the best practice of start recording right before running the stuff you are interested in recording you should never see this. == Macro calls tracing code you don't see on your code When you are evaluating some code that macroexpands to a (do form-1 ... form-n) the compiler recursively calls eval on the sub forms. Because it is tricky in the compiler to tell apart your original source form from the ones the macroexpansion returned those form-1 to form-n get instrumented and then traced as if they were on your code. The tricky part is related to tooling like IDEs sometimes wrapping your forms in some macros that expand to a (do form-1 ... form-n) so we can't simply stop instrumenting after that situation. = Troubleshooting == The outputs panel doesn't work Checkout that you don't have piggieback on the classpath dragged by some dependency. Currently if piggieback is pressent FlowStorm will assume a ClojureScript repl in which the outputs panel isn't supported yet. == Run with JDK 11 FlowStorm UI requires JDK >= 17. If you can't upgrade your JDK you can still use it by downgrading JavaFx. If that is the case add these dependencies to your alias : [,clojure] ---- org.openjfx/javafx-controls {:mvn/version "19.0.2"} org.openjfx/javafx-base {:mvn/version "19.0.2"} org.openjfx/javafx-graphics {:mvn/version "19.0.2"} org.openjfx/javafx-web {:mvn/version "19.0.2"} ---- = Internals, diagrams and documentation For people interested in enhancing, troubleshooting, fixing or just learning about FlowStorm internals take a look at here : https://github.com/flow-storm/flow-storm-debugger/blob/master/docs/dev_notes.md Some useful diagrams : - https://raw.githubusercontent.com/flow-storm/flow-storm-debugger/master/docs/high_level_diagram.svg - https://raw.githubusercontent.com/flow-storm/flow-storm-debugger/master/docs/timeline.svg - https://raw.githubusercontent.com/flow-storm/flow-storm-debugger/master/docs/run_configs.svg //// Local Variables: mode: outline outline-regexp: "[=]+" End: //// ================================================ FILE: docs/user_guide.html ================================================ FlowStorm debugger User’s Guide

FlowStorm is a tracing debugger for Clojure and ClojureScript.

intro screenshot

It can instrument any Clojure code and provides many tools to explore and analyze your programs executions.

1. Quick start

Before you start check FlowStorm minimum requirements.

Important
Minimum requirements
  • jdk >= 17 (if you still need to run it with jdk11 take a look at here)

  • Clojure >= 1.10.0

1.1. Clojure

There are two ways of using FlowStorm for Clojure :

  • With ClojureStorm (recommended) : Swap your Clojure compiler at dev time by ClojureStorm and get everything instrumented automatically

  • Vanilla FlowStorm : Just add FlowStorm to your dev classpath and instrument by tagging and re-evaluating forms

1.1.1. ClojureStorm

This is the newest and simplest way of using FlowStorm. It requires you to swap your official Clojure compiler by ClojureStorm only at dev time.

Swapping compilers sounds like a lot, but don’t worry, ClojureStorm is just a patch applied over the official compiler with some extra stuff for automatic instrumentation. You shouldn’t encounter any differences, it is only for dev, and you can swap it back and forth by starting your repl with a different alias or lein profile.

The easiest way to run and learn FlowStorm with ClojureStorm is by running the repl tutorial.

Try it with no project and no config

You can start a repl with FlowStorm with a single command like this :

;; on Linux and OSX
clj -Sforce -Sdeps '{:deps {} :aliases {:dev {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"} com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}}}' -A:dev

;; on Windows
clj -Sforce -Sdeps '{:deps {} :aliases {:dev {:classpath-overrides {org.clojure/clojure nil} :extra-deps {com.github.flow-storm/clojure {:mvn/version """1.12.4"""} com.github.flow-storm/flow-storm-dbg {:mvn/version """4.5.9"""}}}}}' -A:dev

Pasting that command on your terminal will bring up a repl with FlowStorm and the compiler swapped by ClojureStorm. When the repl comes up evaluate the :dbg keyword to bring up the UI and then click on Help→Tutorial on the menu for a tour of the basics.

After the tutorial you may want to use it on your projects. You use it by adding a deps.edn alias or lein profile.

The simplest way is to setup it globally, so that is what we are going to do next. You can also add it only to specific projects if they require special configurations.

Global setup as deps.edn aliases

You can setup your global ~/.clojure/deps.edn (on linux and macOS) or %USERPROFILE%\.clojure\deps.edn (on windows) like this :

{...
 :aliases
 {:1.12-storm {:classpath-overrides {org.clojure/clojure nil}
               :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"}
                            com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}

  ;; Optional plugins you find yourself using regularly
  :fs-web-plugin {:extra-deps {com.github.flow-storm/flow-storm-web-plugin {:mvn/version "1.0.0-beta"}}
                  :jvm-opts ["-Dclojure.storm.instrumentOnlyPrefixes.webPlugin=org.httpkit.server,ring.adapter.jetty,next.jdbc.result-set"
                             "-Dflowstorm.plugins.namespaces.webPlugin=flow-storm.plugins.web.all"]}

  ...}}

Then you can start your repls with the :1.12-storm alias (like clj -A:1.12-storm). When the repl comes up evaluate the :dbg keyword to bring up the UI, then click on Help→Tutorial on the menu for a tour of the basics.

Global setup as leiningen profiles

You can setup your global ~/.lein/profiles.clj (on linux and macOS) or %USERPROFILE%\.lein\profiles.clj (on windows) like this :

{:1.12-storm
 {:dependencies [[com.github.flow-storm/clojure "1.12.4"]
                 [com.github.flow-storm/flow-storm-dbg "4.5.9"]]
  :exclusions [org.clojure/clojure]}

 ;; Optional plugins you find yourself using regularly
 :fs-web-plugin
 {:dependencies [[com.github.flow-storm/flow-storm-web-plugin "1.0.0-beta"]]
  :jvm-opts ["-Dclojure.storm.instrumentOnlyPrefixes.webPlugin=org.httpkit.server,ring.adapter.jetty,next.jdbc.result-set"
             "-Dflowstorm.plugins.namespaces.webPlugin=flow-storm.plugins.web.all"]}
...}

Then you can start your project repls with +1.12-storm profile (like lein with-profile +1.12-storm repl). When the repl comes up evaluate the :dbg keyword to bring up the UI, then click on Help→Tutorial on the menu for a tour of the basics.

Note
Running lein repl without a project

For some reason if you run lein with-profile +1.12-storm repl outside of a project it will not run with the profile activated correctly.

Per project setup with deps.edn

If your project is using deps.edn, you can add an alias that looks like this :

{...
 :aliases {:1.12-storm
           {;; for disabling the official compiler
            :classpath-overrides {org.clojure/clojure nil}
            :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"}
                         com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}}}

Once you have setup your deps.edn, start your repl with the :1.12-storm alias and run the debugger by evaluating the :dbg keyworkd on your repl (this means just type :dbg and hit return).

If it is your first time using FlowStorm, when the UI comes up click on Help→Tutorial on the menu for a tour of the basics.

If you need more fine control over instrumentation see controlling instrumentation.

Setup with leiningen

If your project uses lein, you can add a profile that looks like this :

(defproject my.project "1.0.0"
  :profiles {:1.12-storm
             {:dependencies [[com.github.flow-storm/clojure "1.12.4"]
                             [com.github.flow-storm/flow-storm-dbg "4.5.9"]]
              :exclusions [org.clojure/clojure]}}
  ...)

Once you have setup your lein profile globally or per project, start your repl with the 1.12-storm profile and run the debugger by evaluating the :dbg keyworkd on your repl (this means just type :dbg and hit return).

Make sure you activate the profile with lein with-profile +1.12-storm repl.

If it is your first time using FlowStorm, when the UI comes up click on Help→Tutorial on the menu for a tour of the basics.

If you need more fine control over instrumentation see controlling instrumentation.

Note
lein dependencies

If you are using lein < 2.11.0 make sure your global :dependencies don’t include the official org.clojure/clojure dependency. Moving to lein latest version should work ok even if your global :dependencies contains the Clojure dep.

1.1.2. Vanilla FlowStorm

If you use the clojure cli you can start a repl with the FlowStorm dependency loaded like this :

;; on Linux and OSX
clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}'

;; on Windows
clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version """4.5.9"""}}}'

If you are a lein user add the dependency to your project.clj :dependencies and run lein repl.

Then require the api namespace and start the debugger :

user> (require '[flow-storm.api :as fs-api]) ;; the only namespace you need to require

user> (fs-api/local-connect) ;; will run the debugger GUI and get everything ready

You should now see a empty debugger window. Click on the recording button to leave the debugger in recording mode and the let’s debug something:

user> #rtrace (reduce + (map inc (range 10))) ;; #rtrace will instrument and run some code

After running it, you should get the return value of the expression (as if #rtrace wasn’t there), but now you will also have the debugger UI showing your recordings.

From here you probably want to check out the Flows tool which contains a lot of information about exploring your recordings.

1.2. ClojureScript

Debugging ClojureScript is a case of remote debugging in FlowStorm. This means the debugger will run in a separate process and connect to the debuggee (your browser or nodejs runtime) via a websocket and optionally an nrepl server.

There are two ways of using FlowStorm with ClojureScript :

  • With ClojureScriptStorm (recommended) : Swap your ClojureScript compiler by ClojureScriptStorm at dev and get everything instrumented automatically

  • Vanilla FlowStorm : Just add FlowStorm to your dev classpath and instrument by tagging and re-evaluating forms

ClojureScriptStorm is a fork of the official ClojureScript compiler that adds automatic instrumentation so you don’t need to think about it (you can still disable it when you don’t need it).

You use it by swapping the official ClojureScript compiler by ClojureScriptStorm at dev time, using dev aliases or profiles.

Note
Repl connection

For enabling every debugger feature, FlowStorm needs to connect to a cljs repl. Currently only shadow-cljs repl over nrepl is supported.

1.2.1. ClojureScriptStorm with shadow-cljs

Important
Minimum requirements
  • For ClojureScript 1.11.* shadow-cljs >= 2.25.4, For ClojureScript 1.12.* shadow-cljs >= 3.1.1

  • FlowStorm >= 3.7.4

For setting up FlowStorm with shadow-cljs you need to modify two files, your shadow-cljs.edn and your deps.edn. This is setup once and forget, so once you have configured FlowStorm you can do everything from the UI, without any other sources modifications.

If you want a shadow-cljs template to play with, take a look at this repo.

Note
shadow-cljs

Currently you can only use ClojureScriptStorm with shadow-cljs if you are resolving your dependencies with deps.edn. This means having :deps true or similar in your shadow-cljs.edn. If you have your dependencies directly in your shadow-cljs.edn you will have to use Vanilla FlowStorm for now. This is because there is currently no way to swap the ClojureScript compiler in shadow-cljs.edn.

First, make your shadow-cljs.edn looks something like this :

{:deps {:aliases [:1.12-cljs-storm]}
 :nrepl {:port 9000}
 ...
 :builds
 {:my-app {...
           :devtools {:preloads [flow-storm.storm-preload]
                      :http-port 8021}}}}

So, the important parts are: you need to tell shadow to apply your deps.edn :1.12-cljs-storm alias, set up a nrepl port, and also add flow-storm.storm-preload to your preloads. If you have other preloads make sure flow-storm.storm-preload is the first one.

Then, modify your deps.edn dev profile to look like this :

{...
 :aliases
 ;; this alias can be defined globally in your ~/.clojure/deps.edn so you don't have to modify this file in your project
 {:1.12-cljs-storm
    {:classpath-overrides {org.clojure/clojurescript nil} ;; disable the official compiler
     :extra-deps {thheller/shadow-cljs {:mvn/version "3.3.4"
                                        :exclusions [org.clojure/clojurescript]}
                  ;; bring ClojureScriptStorm
                  com.github.flow-storm/clojurescript {:mvn/version "1.12.134-3"}
                  ;; add FlowStorm runtime dep
                  com.github.flow-storm/flow-storm-inst {:mvn/version "4.5.9"}}}}}

There are lots of things going on there, but the main ones are: disabling the official compiler, adding ClojureScriptStorm and FlowStorm dependencies, and then configuring what you want ClojureScriptStorm to automatically instrument.

By default the JVM property cljs.storm.instrumentAutoPrefixes is true so all your project top level namespaces will be instrumented automatically.

If you need to set that property to false it is important to configure what namespaces you want to instrument, and you do this by setting the cljs.storm.instrumentOnlyPrefixes jvm property.

This is a comma separated list of namespaces prefixes, you normally want your app namespaces plus some libraries, like : cljs.storm.instrumentOnlyPrefixes=org.my-app,org.my-lib,hiccup

And this is it. Once you have it configured, run your shadow watch as you normally do, load your app on the browser (or nodejs).

Whenever your need the debugger, on a terminal run the ui with your shadow-cljs.edn data :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-app

and then reload you page so it connects to it.

Since we started the app with flowstorm.startRecording=false you will have to click on the record button once to start recording. Whenever recording is enable and something executes under an instrumented namespace you should see the recordings appear in the debugger under the main thread.

Note
recording expressions typed on the repl

If you type at the repl something like (defn foo [a b] (+ a b)) under an instrumented ns, the foo function will get instrumented automatically and you will able to explore the recordings after the function is called. On the other side, typing a simple expression like (+ 1 2) will not show anything, this is currently a limitation but you can still make that work by wrapping the expression on a fn and immediately calling it, like fn [] (+ 1 2)

1.2.2. ClojureScriptStorm with cljs.main

You can use FlowStorm and ClojureScriptStorm with cljs.main.

The easiest way to try it is just by starting a repl, like this :

clj -Sforce -J-Dcljs.storm.instrumentOnlyPrefixes=cljs.user -Sdeps '{:deps {com.github.flow-storm/clojurescript {:mvn/version "1.12.134-3"} com.github.flow-storm/flow-storm-inst {:mvn/version "4.5.9"}}}' -M -m cljs.main -co '{:preloads [flow-storm.storm-preload]}' --repl

If you run the command above you are running cljs.main --repl which will start a ClojureScript repl on your terminal and open a browser connected to it. You runtime will also start with FlowStorm preloaded and everything under cljs.user is going to be instrumented.

Then on a different terminal run the FlowStorm UI :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger

And now refresh your browser page so your browser app connects to the UI.

Note
Limitations

There are some small limitations like not being able to modify instrumentation from the UI without restarting the repl. This is because the FlowStorm UI needs to also connect via nrepl to JVM process running the compiler, which isn’t available when running cljs.main.

1.2.3. ClojureScript vanilla FlowStorm

Let’s say you are using shadow-cljs to start a ClojureScript repl.

First you need to add FlowStorm dependency to your project dependencies, like this :

$ cat shadow-cljs.edn

{...
 :dependencies [... [com.github.flow-storm/flow-storm-inst "4.5.9"]]

 ;; the next two lines aren't needed but pretty convenient
 :nrepl {:port 9000}
 :my-build-id {:devtools {:preloads [flow-storm.preload]}}
 ...}

Then let’s say you start your repl like :

npx shadow-cljs watch :my-build-id

shadow-cljs - config: /home/jmonetta/demo/shadow-cljs.edn
shadow-cljs - server version: 2.19.0 running at http://localhost:9630
shadow-cljs - nREPL server started on port 9000
shadow-cljs - watching build :my-build-id
[:my-build-id] Configuring build.
[:my-build-id] Compiling ...
[:my-build-id] Build completed. (127 files, 0 compiled, 0 warnings, 6.19s)

cljs.user=>

As you can see from the output log shadow-cljs started a nrepl server on port 9000, this is the port FlowStorm needs to connect to, so to start the debugger and connect to it you run :

;; on linux and mac-os
clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-build-id

;; on windows
clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version """4.5.9"""}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-build-id

And that is all you need, the debugger GUI will pop up and everything will be ready.

Try tracing some code from the repl :

cljs.user> #rtrace (reduce + (map inc (range 10))) ;; #rtrace will instrument and run some code

After running it, you should get the return value of the expression (as if #rtrace wasn’t there).

The debugger thread list (the one on the left) shows all the threads it has recordings for. Because we are in javascript land there will always be just one thread, called main. Double clicking it should open the "thread exploring tools" for that thread in a new tab.

This guide will cover all the tools in more detail but if you are interested in code stepping for example you will find it in the code stepping tool at the bottom left corner of the thread tab, the one that has the () icon.

Click on it and use the stepping controls to step over the code.

Now that everything seems to be working move on and explore the many features FlowStorm provides. There are many ways of instrumenting your code, and many ways to explore its executions.

If you are not using a repl or the repl you are using isn’t supported by FlowStorm yet you can still use the debugger but not all features will be supported (mainly the browser features).

For this you can start the debugger like before but without any parameters, like this :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger

And then go to your app code and call (flow-storm.runtime.debuggers-api/remote-connect) maybe on your main, so every time your program starts will automatically connect to the repl.

Note
ClojureScript environments

FlowStorm is supported for ClojureScript in :

  • Browsers

  • NodeJS

  • React native

Note
NodeJs and react-native

On NodeJs and react-native you need to install the websocket library. Do this by running npm install websocket --save

For react-native if your app is running inside a cellphone you will have to also provide the :debugger-host key to flow-storm.debugger.main/start-debugger with your box ip address, unless you are using adb reverse with your ports for which you will have to adb reverse tcp:7722 tcp:7722 (the debugger websocket port)

Note
App initialization debugging

If you need to debug some app initialization, for adding #trace tags before the debugger is connected you will have to require flow-storm.api yourself, probably in your main. All the tracing will be replayed to the debugger once it is connected.

Here is a repo you can use if you want to try FlowStorm with shadow-cljs https://github.com/flow-storm/shadow-flow-storm-basic

1.2.4. Multiple ClojureScript builds

You can setup FlowStorm to debug multiple ClojureScript builds. This can be useful when your application is made up of multiple parts, like when you have web workers.

Debugging multiple builds require multiple debugger instances, one per build.

The FlowStorm UI will start a websocket server, by default on 7722, so if you want to run multiple instances of it, you need to run each instance under a different port. You can do this by providing a :ws-port to the startup command.

So let’s say you want to run two debuggers, one for your page and one for a webworker, your can run them like this :

# on one terminal start your app debugger instance
clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-app :ws-port 7722

# on a second terminal start your webworker debugger instance
clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-web-worker :ws-port 7733

Now you also need to configure your builds to tell them what port they should connect to. You do this by writing different preloads for each of your builds, and then using them instead of your flow-storm.storm-preload, like:

my_app.main_storm_preload.cljs

(ns my-app.main-storm-preload
  (:require [cljs.storm.tracer]
            [flow-storm.tracer :as tracer]
            [flow-storm.runtime.debuggers-api :as dbg-api]))

(dbg-api/start-runtime)
(tracer/hook-clojurescript-storm)
(dbg-api/remote-connect {:debugger-host "localhost" :debugger-ws-port 7722})

my_app.webworker_storm_preload.cljs

(ns my-app.webworker-storm-preload
  (:require [cljs.storm.tracer]
            [flow-storm.tracer :as tracer]
            [flow-storm.runtime.debuggers-api :as dbg-api]))

(dbg-api/start-runtime)
(tracer/hook-clojurescript-storm)
(dbg-api/remote-connect {:debugger-host "localhost" :debugger-ws-port 7733})

They are the same as flow-storm.storm-preload just with different port numbers.

Now you can configure your shadow-cljs.edn like this :

{...
 :builds
 {:app
  {:target :browser
   ...
   :modules
   {:my-app {:init-fn my.app/init
           :preloads [my-app.main-storm-preload]}
    :my-webworker {:init-fn my.app.worker/init
                   :preloads [my-app.webworker-storm-preload]
                   :web-worker true}}}}}
Note
Multiple debuggers tips

You can change the theme or customize the styles of different instances to make it easier to know which debugger instance is connected to which application.

1.3. Babashka

You can debug your babashka scripts with FlowStorm using the JVM. The process is quite simple.

Let’s say we want to debug this example script https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj which runs a webserver with a basic todo app.

First we need to generate a deps.edn by running bb print-deps > deps.edn

Then modify the resulting deps.edn to add the FlowStorm alias like this :

{...
 :aliases {:dev {:classpath-overrides {org.clojure/clojure nil} ;; for disabling the official compiler
                 :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"}
                              com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}
                 :jvm-opts ["-Dclojure.storm.instrumentOnlyPrefixes=user"]}}}

With clojure.storm.instrumentOnlyPrefixes=user we are telling ClojureStorm to instrument everything inside the user namespace since the script doesn’t contain any namespace declaration.

And that is it, you can now start your clojure repl as usual, with clj -A:dev and then eval the :dbg keyword to start the debugger UI.

Then eval the entire file to compile everything. To start the server in this example you will have to remove the wrapping that is basically only allowing the server to run if we are running from babashka, like this :

(when true #_(= *file* (System/getProperty "babashka.file"))
  ...)

so we can also start it from Clojure.

After the server has started, you can use the app from the browser and everything will get recorded as usual.

2. The tool bar

The toolbar as well as the menu provides quick access to some general commands.

toolbar

From left to right:

  • Cancel current running task. Whenever you a running a task that can take some time, this button will be red, and you can use it to cancel the task.

  • The Inst enable button allows to enable/disable instrumentation when in a Storm environment. A change on instrumentation will only affect newly compiled code.

3. Flows tool

The Flows vertical tab contains a bunch of tools for recording and analyzing your programs executions.

First of all, what are Flows?

A Flow is an "execution flow" recording unit. The only purpose of a flow is to group recording activity. This grouping allows us for example to run some code and record it under flow-0, then modify our code, run it again, and record this second run (or flow) under flow-1. Now we can access both recordings separately.

recording controls

When you first open FlowStorm UI you will see four things, from left to right :

  • Clear your recordings if any.

  • Start/Stop recording. You can keep your heap from growing by stopping recording when you don’t need it.

  • Start/Stop recording the multi-thread timeline. Check out the multi-thread timeline tool.

  • The Rec on combo-box to select under what flow new recordings are going to be stored.

Whenever there is something recorded for a flow, a new tab with the flow name will appear.

Execution inside a flow will be grouped by threads. So the first thing you will see on a flow is a menu of threads we have recordings for so far. This threads will be referred sometimes as timelines, since they are a sequence of recorded execution steps.

Let’s say for example we have selected to record under flow-1 and run some multi threaded code.

We are going to see something like this :

multi flows 1

There is a lot going on in the screenshot above, but the most important are :

  • we have configured FlowStorm to record new executions under flow-1

  • we have recorded stuff under flow-1 and there are also some previous recordings under flow-0

  • we are currently looking at flow-1, we have opened to explore the thread with id 1 called main and we are exploring it in the code stepper

  • Threads [4] indicates we have recorded activity in 4 threads, which we can access via this menu

Now for a different example :

multi flows 2

This second image shows us exploring the recordings of a thread with id 474, called pool-4-thread-4 on flow-0.

flows toolbar

The Flows tool also contains a toolbar that contains the Quick jump box. Use it for quickly opening the first recording of a function in the code stepper. Will autocomplete the first 25 matches.

In the screenshot above we see analyzing the recordings in the code stepper but there are many tools to explore the recorded timelines, which we are going to describe next.

3.1. Code tool

code tool tab

The code tool is the first of the Flows tab. It provides most of the functionality found in a traditional debugger. You can use it to step over each expression, visualize values, locals and more.

3.1.1. Code stepping

The code tool allows you to step and "travel throught time" in two ways:

  • Use the controls at the top to step over your code in different ways.

  • Click on the highlighted forms to position the debugger at that point in time.

controls

For moving around using the controls we have two rows of buttons.

The second row of controls, the most important one, are the stepping controls.

From left to right they are :

  • Step over backwards, will make one step backwards always staying on the same frame.

  • Step backwards, will step backwards in time going into sub functions.

  • Step out, will position the debugger in the next step after this function was called.

  • Step forward, will step forward in time going into sub functions.

  • Step over forward, will make one step forwards always staying on the same frame.

The numbers at the center show current_step_index / total_steps. This means that a total of total_steps has been recorded for this thread so far. Write any number (less than total_steps) on the text box to jump into that position in time.

The buttons around the step counter are :

  • Jump to the first step of the recording.

  • Jump to the last step of the recording.

On the first row we have more controls, also for moving around in time.

From left to right we have :

  • Undo navigation

  • Redo navigation

  • Add a bookmark

  • The last stepping controls to the right are the power stepping controls.

Note
Highlighting

Only the forms that were executed at least once for the current function frame will be highlighted.

This means that code can be un-highlighted for two reasons:

  • there isn’t any recording for that part of the code

  • there is a recording but doesn’t belong to this function frame.

stepper highlighting

In the contrived example above we see we are stepping the foo function. All inside this function body is highlighted but the bodies of the two anonymous functions for mapping and reducing. This will only get highlighted once you step into their bodies.

In this case you are sure there are recordings for these functions bodies because the reduce is non lazy, so if you keep stepping eventually you will get into their bodies, but there is a faster way.

stepper highlighting 2

For this you can right click on any un-highlighted expression that you think there could be a recording for and select Jump forward here.

stepper highlighting 3

This will make FlowStorm scan from the current point of the timeline searching forward for a value recorded at that coordinate (if any) and move the stepper to that point in time.

You also have Jump to first record here which will scan from the beginning of the timeline and Jump backwards here which will search backwards from the current position.

3.1.2. Power stepping

controls power custom

The controls at the right are power stepping controls. They provide more powerfull ways of stepping through the code.

Clicking on the first, back, next or last buttons will navigate the timeline using the selected power stepping tool in the dropdown.

There are currently six power stepping tools :

  • identity, will step to the prev/next value which identity is the same as the current value.

  • 'equality', will step to the prev/next value which is equals (clojure equality) to the current value.

  • same-coord will step to the prev/next value for the same coordinate. This means it will move to the next recording in the timeline for this exact place in the code you are currently in. You can also see it as take me to all the situations when the current expression executed doesn’t matter how we got to it.

  • custom, allows you to provide a predicate, which will be used to find the next step. If you define it like (fn [v] (map? v)) will make the power stepper step over all map values.

  • custom-same-coord, the same as custom but fixed on the current coordinate like same-coord.

  • identity-other-thread, will step to a position which identity is the same as the current value in a different thread. Here the prev and next arrows do the same thing, it will just jump to the first position that matches this value on a different thread. This has some limitations. If there are more than two threads working with this identity there is no way of choosing which thread to go. If you need more control, checkout the programmable debugging section, specially the find-expr-entry function.

  • fn-call, allows you to provide a function to step to.

Note
Custom stepping

Custom power stepping is only supported in Clojure now.

Power stepping automatically skips all values equals to :flow-storm.power-step/skip. This can be useful when combined with snapshot-value as a way of ignoring some of them, which provides a way of sampling tight loops like in games.

3.1.3. Searching

search access

You can use the search tool to search over all your flow recorded expressions and then make the stepper jump to them. You can find the search tool under More tools → Search.

There are multiple ways of searching:

  • By pr-str

  • By data window current value

  • By predicate

Searching by pr-str
search pr str

This type of search will walk over the selected threads expressions, converting their values to strings with pr-str up to the selected level and depth and then checking if the resulting string contains your provided query string.

Searching by DataWindow value
search data window

Searching by data window value allows you to select any of the current data windows and will search for the current selected data window value over the selected threads expressions values using identity.

Searching by predicate
search pred

Searching by predicate allows you to provide a Clojure predicate which will be used over all selected threads expressions values.

3.1.4. Loops

Whenever you click a highlighted form that has been executed multiple times inside the same function call (any kind of loop), instead of immediately jumping into it, FlowStorm will popup a menu, like in the picture below :

loops

This is the loops navigation menu. It allows you to quickly move around interesting iterations of the loop.

The menu will display slightly different options depending on you current position. The [FIRST] …​ and [LAST] …​ entries will always show, which allows you to quickly jump to the first and last iteration of the loop.

If you are currently before the loop, clicking into any expression inside the loop will show the first 20 values for the clicked expression.

If instead you are currently in a expression after the loop, clicking back to an expression inside the loop, will show the last 20 values for the clicked expression.

Now if you are currently stepping inside the loop, clicking any other expression inside it will show you 10 values before and 10 values after of your current position.

Clicking on any of this entries will take you to that position in time.

If this is not enough, and you want to see all the values taken by some expression along the loop, you can always use the printer tool.

3.1.5. Exceptions debugging

FlowStorm will capture all functions that didn’t return because an exception unwind the stack, even when that exception was captured further and it didn’t bubble up.

exceptions

When an unwind situation is recorded a combobox will show up in the toolbar, containing the functions names together with the exceptions types. If you hover the mouse over any of them, a tooltip will display the exception message.

Clicking on any of them will position the stepper at that point in time so you can explore what happened before.

You can configure FlowStorm to automatically jump to exceptions with the Config menu by checking Auto jump to exception which is disabled by default.

3.1.6. Locals

The locals panel will show the locals visible for the current point in time and their values at binding time.

locals

Right clicking on them will show a menu where you can :

  • define all

  • define the value with a name, so you can use it at the repl

  • inspect the value with a data window

  • tap the value as with tap>

Define all will define all the bindings currently visible in the locals pane in the current form namespace. This is useful for trying things at your editor as described here https://www.cognitect.com/blog/2017/6/5/repl-debugging-no-stacktrace-required

Note
Locals and mutable values

The Locals pane will show the value of each binding for a symbol at binding time, which are the same thing no matter where you are in the current block when working with immutable objects, but not when working with mutable ones. If what was bound was muttable in any way, you will be seeing the value at binding time, and not at current time.

3.1.7. Stack

The stack panel will always show the current stacktrace. Be aware that the stacktrace only include functions calls that had been recorded, so if you aren’t recording everything there will be gaps.

stack

Double clicking on any of the stack entries will make the debugger jump to that point in time.

3.1.8. Value panels

Value panels show in many places in FlowStorm.

value panels2

The value panel in the code tool always display a pretty print of the current expression value.

You can configure the print-level and print-meta for the pretty printing by using the controls at the top.

The value panel showing the current expression in the code stepper is a little bit special since it also contains a data window tab which allows you to quickly navigate the value or give it custom visualizations.

value panels1
Define value for repl

Use the def button to define a var pointing to the current inspector value.

You can use / to provide a namespace, otherwise will be defined under [cljs.]user

3.1.9. Goto to file:line

Clicking on the Actions→Goto file:line menu allows you to search and jump to the first recording of a expression with a file and line, given that one exists.

It will ask you for a file and line in the format of <class-path-file-path>:<line>.

If you have a file like src/org/my_app/core.clj and you are interested in expressions evaluating on like 42 you should search like org/my_app/core.clj:42.

3.2. Call Stack tree tool

The call stack tree tool is the second one of the Flows tab. It allows you to see the execution flow by expanding its call stack tree.

callstack tool tab

The call stack tree is useful for a high level overview of a complex execution and also as a tool for quickly moving through time.

You can jump to any point in time by double clicking on a node or by right clicking and on the context menu selecting Step code.

callstack tree

Use the button at the top left corner of the tree tool to show the current frame of the debugger in the tree.

There are also two value panels at the bottom that show the arguments and return value for the currently selected function call.

Note
Disabling the call stack tree tool

The call stack tree tool can be enable/disable on the fly if you are not using it and performance is an issue, since keeping it updated can be expensive. You can disable it from the Config menu or via the flowstorm.callTreeUpdate=false JVM prop.

3.3. Functions tool

The functions tool is the third one of the Flows tab.

functions tool tab

It shows a list of all traced functions sort by how many times the have been called.

functions calls

Normal functions will be colored black, multimethods magenta and types/records protocols/interfaces implementations in green.

Together with the call stack tree it provides a high level overview of a thread execution, and allows you to jump through time much faster than single stepping.

You can search over the functions list by using the bar at the top.

3.3.1. Function calls

Clicking on the calls counter of any function will display all function calls on the right sorted by time. Each line will show the arguments vector for each call, and their return value. Use the check boxes at the top to hide some of the arguments.

function calls

Double clicking on any row in the functions call list will jump to the stepper at that point in time.

You can also use the args and ret buttons to open the values on the inspector.

3.4. Multi-thread timeline

You can use this tool to record, display and navigate a total order of your recordings in a timeline. This can be used, for example, to visualize how multiple threads expressions interleave, which is sometimes useful to debug race conditions.

You enable/disable the multi-thread timeline recording using its button on the toolbar. Recording on the multi-thread timeline will make your program execution a little slower so it is recommended to have it paused unless you need it.

When you have something recorded on the multi-thread timeline you access the tool from the top right corner.

multi timeline access

As an example, let’s say you record the execution this function :

(defn run-parallel []
  (->> (range 4)
       (pmap (fn [i] (factorial i)))
       (reduce +)))

By opening the tool a window like this should pop up :

timeline

As you can see the timeline tool displays a linear representation of your expressions. Times flows from top to bottom and each thread gets assigned a different color. Every time a function is called or returns you will see it under the Function column, and for each expression executed you will see a row with its Expression and Value.

Double clicking any row will make your code stepper (on the main window) jump to the code at that point in time.

Note
Big recordings timeline

Rendering the timeline needs some processing to render each sub-form and print each value so be aware it could be slow if you try it on big recordings.

There is also a Only functions? checkbox at the top that will retrieve only function calls and can be used to visualize the threads interleaving at a higher level.

3.5. Printer

FlowStorm has a lot of functionality to replace printing to the console as a debugging method since most of the time it is pretty inefficient. Nonetheless, sometimes adding a bunch of print lines to specific places in your code is a very powerful way of understanding execution.

For this cases FlowStorm has the Printer tool, which allows you to define, manage and visualize print points, without the need of re running your code. It will work on your recordings as everything else.

You can add and re run print points over your recordings as many times as you need. To add a print point, just right click on any recorded expression.

printer add

It will ask you for a couple optional fields.

printer add box

The Message format is the "println text". A message to identify the print on the printer output. Here you can use any text, in which you can optionally use %s for the printed value, same as you would use it with format.

The Expression field can be use to apply a transformer function over the value before printing it. Useful when you want to see a part of the value.

printer access

After you add them, you can access the Printers tool by navigating to More tools → Printers.

The threads selector allows you to select the thread the prints are going to run on. Leaving it blank will run prints over all threads recordings (checkout the notes for caveats). Clicking the refresh button will [re]run the printing again over the current recordings.

printer

You can tweak your prints at any time, like changing the print-length, print-level, message, transform-fn or just temporarily disable any of them. When you are ok re-setting you prints, just click refresh and they will print again.

Double clicking on any printed line will jump to the Flows code tab, with the debugger pointed to the expression that generated the print.

Important
Multi-thread prints order

If you select All threads, and have a multi-thread timeline recording, then the printer will use it and you can use prints to debug threads interleaving for example, but if you run your printers with All threads selected without a multi-thread timeline recording they will print sorted by thread and not in the order they happened.

3.6. Bookmarks

Bookmarks are another quick way of jumping around in code and they can be added from your code or the FlowStorm UI.

You can find you bookmarks on the top menu View → Bookmarks.

bookmarks

Double clicking on any bookmark will make the debugger jump back to its position.

3.6.1. Code bookmarks

You add code bookmarks by adding the (bookmark) statement to your code, which optionally accepts a label.

The first time a bookmark statement is executed it will make the FlowStorm UI jump to it. Since this behavior is similar to a debugger statement in languages like Javascript, it is also aliased as (debugger) so you can use whichever you prefer.

Note
ClojureScript support

This is currently only supported when using ClojureScriptStorm >= 1.11.132-9

3.6.2. UI bookmarks

UI bookmarks are useful when you find yourself jumping around, trying to understand a complex execution. They enable you to mark execution positions so you can come back to them later.

bookmarks add btn

You can bookmark the current position by pressing the bookmark button in the code tool, next to your stepping controls. It will ask you the bookmark description.

4. Browser tool

The browser tool is pretty straight forward. It allows you to navigate your namespaces and vars, and provides ways of managing what gets instrumented.

browser

5. Outputs tool

outputs

The outputs tool can be used instead of your normal IDE/Editor panel to visualize your evaluations results, your taps outputs and your out and err streams writes (like printlns).

The advantages being :

  • Custom visualizations

  • Quick nested values navigation

  • Quick taps values navigation

  • Datafy nav navigation

  • Access to all previously tapped values

  • Access to the last 10 evaluated values (instead of just *1 and *2)

  • Ability to search tapped values in Flows

The taps visualization system works out of the box while the evals result and printing capture currently depends on you using nrepl and starting with the flow-storm middleware. Checkout the outputs setup section for instructions.

Note
ClojureScript support

Only the taps viewer is currently supported on ClojureScript. The last evaluations and the out and err streams capture aren’t supported yet.

5.1. Middleware setup

For using all the features in the Outputs tool you need to be using nrepl and start your repl with flow-storm.nrepl.middleware/wrap-flow-storm middleware.

If you use Cider for example you can add it to cider-jack-in-nrepl-middlewares via customizing the global value or by using .dir-locals.el.

5.2. Output data window

The top panel is a data window for displaying evaluations and taps. As soon as you evaluate or tap something it will be displayed here.

5.3. Last evals

The last evals pane gives you access to the last 10 evaluation results, same as *1 and *2.

Click on any value to display it on the top data window.

5.4. Taps

Everytime FlowStorm starts, it will add a tap, so whenever you tap> something it will show on the taps list.

Click on any value to display it on the top data window.

If the tapped value has also been recorded as an expression in Flows, you can right click on it and run Search value on Flows to move the debugger to that point in time.

Note
Search value on Flows

Be aware that if the code that taps your value is something like (tap> :a-key) you won’t be able to jump to it using this, because :a-key isn’t a value recorded by FlowStorm, while if the tapping code is like (tap> some-bind) or (tap> (+ 2 3)) or the tapping of any other expression you should be able to jump to it. So if you want to use this functionality as a "mark" so you can quickly jump to different parts of the recordings from the Taps tool, you can do it like (tap> (str :my-mark))

A #tap tag will also be available, which will tap and return so you can use it like (+ 1 2 #tap (* 3 4)) Use the clear button to clear the list.

There is also #tap-stack-trace. It will tap the current stack trace.

5.5. Out and Err streams

Everything written on out or err will be captured and displayed on the bottom panel. You can copy anything from this area with normal tools.

6. Data Windows

data window

Data Windows are a user extensible tool to visualize and explore your data. Their role is to support :

  • a way to navigate nested structures in a lazy way

  • visualize and navigate metadata

  • multiple visualizations for each value

  • lazy/infinite sequences navigation

  • a way to define the current sub-values so you can use them at the repl

  • a mechanism for realtime data visualization

  • clojure.datafy navigation out of the box

  • tools for the user to add custom visualizations on the fly

The next sections will explore each of them.

6.1. Data navigation

data window dig

You can navigate into any key or value by clicking on it.

Use the breadcrumbs at the top to navigate back.

6.2. Metadata navigation

data window meta

If any value contains metadata, it will be shown at the top. Clicking on it will make the data window navigate into it.

6.3. Multiple visualizers

data window multiple viz

You can change how to display your current value by using the visualizers selector dropdown at the top.

6.4. Sequences

data window seqable

The seqable visualizer allows you to navigate all kind of sequences (even infinite ones) by bringing more pages on demand.

Click on More to bring the next page in.

6.5. Defining values

You can always define a var for the current value being shown on any data window by clicking the def button. Clicking on it will raise a popup asking for a symbol name. If you don’t provide a fully qualified symbol it will define the var under user or cljs.user if you are in ClojureScript.

A quick way to use it is to provide a short name, let’s say foo, and then access it from your code like user/foo.

6.6. Realtime visualizations

data window realtime

DataWindows not only support displaying and navigating values, but also updating them in real time from your application.

From your program’s code you can always create a data window with :

(flow-storm.api/data-window-push-val :changing-long-dw-id 0 "a-long")

by providing a data window id, a value, and optionally the initial breadcrumb label.

But you can also update it (given that the selected visualizer supports updating like :scope for numbers) with :

(flow-storm.api/data-window-val-update :changing-long-dw-id 0.5)

This data-window-val-update is pretty useful when called from loops or refs watches, specially paired with a custom visualizer.

6.7. Clojure datafy/nav

data window datafy nav

Data Windows support datafy nav out of the box. The data window will always be showing the result of clojure.datafy/datafy of a value. For maps or vectors where keys provide navigation it will automatically add a blue arrow next to the value.

Clicking on the value will just dig the data, while clicking on the blue arrow will navigate as with clojure.datafy/nav applied to that collection on that key.

6.8. EQL pprint visualizer

eql visualizer 0
eql visualizer 1

The eql-query-pprint visualizer allows you to explore your data "entities" by looking at subsets of it using queries similar to datomic pull queries like in the screenshots above.

Note
Disable by default

The EQL query pprint is disable by default. To enable it call (flow-storm.runtime.values/register-eql-query-pprint-extractor).

By entities it means maps which contains only keywords as their keys. Every other collection is just traversed.

This are all valid queries :

  • [*]

  • [:name]

  • [:name :age :vehicles]

  • [:name :age {:vehicles [:type]}]

  • [:name :age {:vehicles [?]}]

  • [:name {:vehicles [*]}]

  • [:name :age {:vehicles [:type {:seats [?]}]}]

  • [:name :age {:vehicles [:type {:seats [:kind]}]}]

  • [:name {:houses [:rooms]}]

The * symbol means include all keys, while the ? symbol means just list the keys, which helps exploring big nested maps with many keys.

6.9. Custom visualizers

An important aspect of Data Windows is to be able to provide custom visualizers on the fly.

Let’s say we have model a chess board as a set of maps which represent our pieces.

(def chess-board
  #{{:kind :king  :player :white :pos [0 5]}
    {:kind :rook  :player :white :pos [5 1]}
    {:kind :pawn  :player :white :pos [5 3]}
    {:kind :king  :player :black :pos [7 2]}
    {:kind :pawn  :player :black :pos [6 6]}
    {:kind :queen :player :black :pos [3 1]}})

(flow-storm.api/data-window-push-val :chess-board-dw chess-board "chess-board")

If we open a data window with data-window-push-val we are going to see something like this :

data window custom1

but we can do better, we can create a custom visualizer so we can see it like this :

data window custom2

Data visualization in FlowStorm is composed of two things:

  • a data aspect extractor, which runs on the runtime process, and will build data for the visualization part

  • a visualizer, which runs on the debugger process, and will render extracted data for a value using javafx

For a basic Clojure session everything will be running under the same process, but this is not the case for ClojureScript or remote Clojure.

First let’s require some namespaces :

(require '[flow-storm.api :as fsa])
(require '[flow-storm.debugger.ui.data-windows.visualizers :as viz])
(require '[flow-storm.runtime.values :as fs-values])

We can register a custom visualizer by calling register-visualizer.

(viz/register-visualizer
     {:id :my-viz
      :pred (fn [val] )
      :on-create (fn [val] {:fx/node :any-java-fx-node-that-renders-the-value
                            :more-ctx-data :anything})
      ;; OPTIONALLY
      :on-update (fn [val created-ctx-map {:keys [new-val]}] )
      :on-destroy (fn [created-ctx-map] )
      })

The important part there are :id, :pred, and :on-create.

The :id will be the one displayed on the visualizers dropdown, and re-registering a visualizer with the same id will replace the previous one.

:pred is a predicate on the data extracted from values, it should return true if this visualizer can handle the value.

And :on-create will be a function that receives this value and renders a java fx node. The val passed to on-create will also contain two special keywords :

  • :flow-storm.debugger.ui.data-windows.data-windows/dw-id The id of the data windows it’s being draw on

  • :flow-storm.debugger.ui.data-windows.data-windows/preferred-size (could be :small)

Optionally you can provide :on-update and :on-destroy.

:on-update will receive values from the runtime via fsa/data-window-val-update. It will also get a handle to the original value (the one that created the DataWindow) and whatever map was returned by :on-create.

:on-destroy will be called everytime a visualizer gets removed, because you swapped your current visualizer or because you went back with breadcrums. It can be useful in case you need to clear resources created by :on-create.

:pred and :on-create will not receive the original value but the extracted aspects of it after all registered extractors run.

You can check the data available to your visualizer for a value in a data window by calling :

(viz/data-window-current-val :chess-board-dw)

If the data already extracted from your value is not enough for your visualizer you can register another extractor.

6.9.1. Data aspect extraction

(fs-values/register-data-aspect-extractor
   {:id :chess-board
    :pred (fn [val _]
            (and (set? val)
                 (let [{:keys [kind player pos]} (first val)]
                   (and kind player pos))))
    :extractor (fn [board _] {:chess/board board})})

In this case we are going to register and extractor that will only run for vals which are sets and contains at least one element which is a map with :kind, :player and :pos. The extracted data will be the entire board.

All ids of extractors that applied for a value will be appended under ::fs-values/kinds of the value as you will see next.

6.9.2. Visualizers

Now we can register a visualizer that will show only on values which contains a :chess-board kind.

(import '[javafx.scene.layout GridPane])
(import '[javafx.scene.control Label])

(viz/register-visualizer
   {:id :chess-board
    ;; only be available if the chess-board data extractor run on this value
    :pred (fn [val] (contains? (::fs-values/kinds val) :chess-board))

    ;; use the chess/board info to render a board with java fx
    :on-create (fn [{:keys [chess/board]}]
                 (let [kind->sprite {:king "♚" :queen "♛" :rook "♜" :bishop "♝" :knight "♞" :pawn "♟"}
                       pos->piece (->> board
                                       (mapv #(vector (:pos %) %))
                                       (into {}))]
                   {:fx/node (let [gp (GridPane.)]
                               (doall
                                (for [row (range 8) col (range 8)]
                                  (let [cell-color (if (zero? (mod (+ col (mod row 2)) 2)) "#f0d9b5" "#b58863")
                                        {:keys [kind player]} (pos->piece [row col])
                                        cell-str (kind->sprite kind "")
                                        player-color (when player (name player))]
                                    (.add gp (doto (Label. cell-str)
                                               (.setStyle (format "-fx-background-color:%s; -fx-font-size:40; -fx-text-fill:%s; -fx-alignment: center;"
                                                                  cell-color player-color))
                                               (.setPrefWidth 50))
                                          (int col)
                                          (int row)))))
                               gp)}))})

6.9.3. ClojureScript

Using custom visualizers with ClojureScript (or other remote environments) is a little bit more involved.

Registering aspect extractors is exaclty the same, since they run on the runtime (browswer, node, etc), but custom visualizers should be registered on the debugger process. For this you need to create your visualizers in some namespace, let’s say on /dev/visualizers.clj, add the dev folder to your classpath and then running the debugger UI with something like :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :my-app :pre-require visualizers

Notice the last option :pre-require visualizers. This will allow you to load the just defined visualizers namespace before starting the UI.

6.10. Default visualizers

You can make any visualizer the default by calling add-default-visualizer which takes a predicate on the val-data (the same received by :on-create) and a visualizer key, like this :

(viz/add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :chess-board)) :chess-board)

For all FlowStorm provided visualizers take a look at flow-storm.debugger.ui.data-windows.visualizers namespace.

Default visualizers predicates are added in a stack, and tried from the top. This means that you can always overwrite a default by adding a new one.

7. Thread breakpoints

thread breaks

FlowStorm is a tracing debugger, which means it can record what is happening without the need of stopping your programs execution. This is all fine but doesn’t cover every possible situation. There are cases where recording everything is impractical, and even if you can start/stop recording whenever you want, being able to automatically stop your threads at certain points is useful.

For these cases, FlowStorm has the ability to set thread breakpoints, which means to define points (functions) in the execution of your program where you want your threads to wait. While the threads are waiting you can explore what happened so far.

As soon as a thread hits a break function, if recording is on, it will be blocked, and a "Threads blocked" menu will show up in the UI. You can use this menu to unblock different threads.

Once you are done, you can pause recording using the pause button in the main toolbar and un-block every thread.

You can define thread breakpoints in two ways :

  • Using the browser (like in the image below), you can navigate to any function and click on the Break button. This will block the calling thread every time the selected function gets called.

  • Or you can also install a break by calling (flow-storm.api/break-at 'my-proj.core/some-fn)

browser breakpoints
Note
Conditional threads breakpoints

The break-at fn accepts a second argument where you can provide a predicate that will be called with the same arguments of the function you are breaking. It will only break when the predicate returns true. If you don’t provide a predicate it will default to (constantly true)

You can remove breakpoints by :

  • Clicking on the browser instrumentation list delete buttons

  • Calling flow-storm.api/remove-break to remove a single breakpoint

  • Calling flow-storm.api/clear-breaks to remove all breakpoints

8. Programmable debugging

FlowStorm gives you full access to its internal indexes from the repl in Clojure and ClojureScript. These allows you to explore your recordings using Clojure and write small programs to analyze them if what’s provided by the GUI is not enough.

Most of what is documented here is also documented in the flow-storm.runtime.indexes.api namespace docstring, which you can retrieve by evaluating (doc flow-storm.runtime.indexes.api). In fact, this is the only namespace you need to require on your repl in order to work with your recordings.

Let’s say you have recorded some execution and now you want to work with the recordings from the repl.

So first we require the api ns as ia.

(require '[flow-storm.runtime.indexes.api :as ia])

Now from the UI, you can get the thread-id of your recordings (the number next to the thread name) which you will need for accessing them from the repl.

8.1. Timelines

Let’s say you want to explore recordings on thread 32. You can retrieve its timeline by calling ia/get-timeline like this :

(def timeline (ia/get-timeline 32))

Once you have the timeline you can start exploring it.

The timeline implements many of the Clojure basic interfaces, so you can :

user> (count timeline)
798

user> (take 3 timeline)
; (#flow-storm/fn-call-trace [Idx: 0 org.my-app/run-server]
;  #flow-storm/fn-call-trace [Idx: 1 org.my-app/read-config]
;  #flow-storm/fn-call-trace [Idx: 2 org.my-app/check-config])

user> (get timeline 0)
; #flow-storm/fn-call-trace [Idx: 0 org.my-app/run-server]

The easiest way to take a look at a thread timeline is with some code like this :

(->> timeline
     (take 3)
     (map ia/as-immutable))

; ({:type :fn-call,
;   :fn-ns "org.my-app",
;   :fn-name "run-server",
;   :ret-idx 797,
;   :fn-call-idx 0,
;   :parent-idx nil,
;   :fn-args [],
;   :form-id -798068730,
;   :idx 0}
;  ...
;  ...)

In most cases converting all entries into maps with ia/as-immutable is enough, but if you want a little bit more performance you can access entries information without creating a immutable map first.

Timelines entries are of 4 different kinds: FnCallTrace, FnReturnTrace, FnUnwindTrace and ExprTrace.

You can access their data by using the following functions depending on the entry :

All kinds :

  • as-immutable

  • fn-call-idx

ExprTrace, FnReturnTrace and FnUnwindTrace :

  • get-coord-vec

ExprTrace, FnReturnTrace :

  • get-expr-val

FnUnwindTrace :

  • get-throwable

FnCallTrace :

  • get-fn-name

  • get-fn-ns

  • get-fn-args

  • get-fn-parent-idx

  • get-fn-ret-idx

  • get-fn-bindings

You can also access the timeline as a tree by calling :

  • callstack-root-node

  • callstack-node-childs

  • callstack-node-frame-data

Take a look at their docstrings for more info.

8.2. Forms

You can retrieve forms by form id with get-form and then use get-sub-form-at-coord and a coordinate.

Here is a little example :

;; retrieve some expression entry into expr
user> (def expr (-> timeline
                    (get 3)
                    ia/as-immutable))

user> expr
{:type :expr, :coord [2 2 1], :result 4, :fn-call-idx 2, :idx 3}

;; retrieve the fn-call entry for our expr
user> (def fn-call (-> timeline
                       (get (:fn-call-idx expr))
                       ia/as-immutable))
user> fn-call
{:type :fn-call,
 :fn-ns "dev-tester"
 :fn-name "other-function",
 :form-id 1451539897,
 ...}

;; grab it's form
user> (def form (-> fn-call
                    :form-id
                    ia/get-form
                    :form/form))
user> form
(def other-function (fn [a b] (+ a b 10)))

;; lets look at the sub-form from form at our expr coordinate
user> (ia/get-sub-form-at-coord form (:coord expr))
a

8.3. Multi-thread timeline

If you have recorded a multi-thread timeline, you can retrieve it with total-order-timeline like this :

(def mt-timeline (ia/total-order-timeline))

which you can then iterate using normal Clojure functions (map, filter, reduce, get, etc).

The easiest way to explore it is again with some code like this :

user> (->> mt-timeline
           (take 3)
           (map ia/as-immutable))

({:thread-id 32,
  :type :fn-call,
  :fn-call-idx 0,
  :fn-ns "org.my-app",
  :fn-name "run",
  :fn-args [],
  :ret-idx 797,
  :parent-idx nil,
  :form-id -798068730,
  :idx 0}
  ...
  ...)

Notice that each of these entries contains a flow-id and thread-id also.

8.4. Other utilities

There are other utitities in the api ns that could be useful, some of the most interesting ones :

  • find-expr-entry useful for searching expressions and return values with different criteria.

  • find-fn-call-entry useful for searching functions calls with different criteria.

  • stack-for-frame

  • fn-call-stats

Take a look at their docstrings for more info.

9. LLM agents

You can teach a LLM how to use FlowStorm’s api to help you analyze your recordings.

If you are using the amazing clojure-mcp you just need to upload one more file that teaches the LLM FlowStorm’s basics from the repl.

Here is a very basic chat asking Claude to look at some recordings of a buggy TODO’s web application.

10. Remote debugging

You can remotely debug any Clojure application that exposes a nrepl server.

In terms of dependencies, the debuggee side should be setup the same as a normal local setup, with the optional change that you can use flow-storm-inst instead of flow-storm-dbg, being the former a slimmed down version of the later one that doesn’t contain some libraries used only by the UI, but using the full flow-storm-dbg is also ok.

10.1. SSH tunnel

The easiest way to debug a remote application is via a ssh tunnel. You can create it from your dev box like this :

ssh -L 9000:localhost:9000 -R 7722:localhost:7722 my-debuggee-box.com

assuming your remote process at my-debuggee-box.com has started a nrepl server listening on port 9000 and that the debugger websocket server is running on the default port.

After the tunnel is established, you can run you debugger UI like this :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000

and that is it.

If you need to connect the debugger to a remote process without a ssh tunnel or you need to configure the websocket server port you can do it like this :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port NREPL-PORT :runtime-host '"YOUR-APP-BOX-IP-ADDRESS"' :debugger-host '"YOUR-BOX-IP-ADDRESS"' :ws-port WS-SERVER-PORT

10.2. Out of process

Sometimes you are not debugging across a network but you want to run the FlowStorm UI on a different process.

A couple of aliases that can help for this :

{:aliases
 ;; for your system process
 {:runtime-storm {:classpath-overrides {org.clojure/clojure nil}
                  :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"}
                               com.github.flow-storm/flow-storm-inst {:mvn/version "4.5.9"}}}
  ;; for the FlowStorm GUI process
  :ui-storm {:extra-deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}
             :exec-fn flow-storm.debugger.main/start-debugger
             :exec-args {:port 7888}}}} ;; set your nrepl port here!

With those aliases you can start your application process by adding :runtime-storm and that is it.

To start the FlowStorm UI, go to another terminal and run clj -X:ui-storm.

10.3. Docker

If you run you process inside a docker container, here is a basic template for using FlowStorm with it https://github.com/jpmonettas/docker-flow-storm-basic

11. Dealing with too many traces

When recording an application’s execution (specially when using ClojureStorm or ClojureScriptStorm) it could happen that your process starts running out of heap. This section documents some tools FlowStorm provides to deal with this situations.

Most of the time, having the recording paused and just enabling it right before executing the action you are interested in is enough, but when it isn’t, here are some other options.

11.1. Fn call limits

A common situation is to see some high frequency functions adding a lot of noise to your recordings. For example a MouseMove event processing will generate a lot of recordings while you use your app. There are a couple of ways to limit your functions calls by thread. You can identify this kind of functions with the functions tool.

One tool you can use in this situations is the flowstorm.threadFnCallLimits JVM prop.

For example, you can add "-Dflowstorm.threadFnCallLimits=org.my-app/fn1:2,org.my-app/fn2:4" so every time the system starts, limits will be set for org.my-app/fn1 and org.my-app/fn2. The number next to them is the limit. When a function reaches the limit FlowStorm will stop recording calls to it and all the functions down its callstack.

You can also modify the limits from your repl, by calling flow-storm.runtime.indexes.api/[add-fn-call-limit|rm-fn-call-limit|get-fn-call-limits]. In ClojureScript you need to call them via your cljs repl api.

These limits are per thread, so when a thread recording is created it will start with the current defined counters, and each time a function gets called the counter will decrement. When it reaches zero the function and all functions calls under it will stop being recorded.

When you clear your threads you are also clearing its limit counters, so next time you record something new counters will be initialized from your global limits definitions.

11.2. Trace and heap limits

If you are tracing some code that ends up in a infinite loop the debugger will choke on too many traces, making everything slow and your only option is probably to restart it.

For preventing this, FlowStorm provides a couple of fuse/breakers, on threads trace count and on heap limits.

They are off by default but you can enable it from the Config menu.

11.2.1. Trace limits

Let’s say you added a thread trace limit of 1000. If you now run any code where a thread generates more than a 1000 traces FlowStorm will only record those first 1000 traces and then discard the rest as if recording is off for that thread, while it will keep recording threads that haven’t reached the limit.

Your code will continue execution as normal, which you can break using your normal editor breaking commands if its an infinite loop, but now you have recordings to look at what is going on.

You can set a limit of 0 to disable it again.

You can set this limits at startup via the JVM options "-Dflowstorm.threadTraceLimit=1000" and "-Dflowstorm.throwOnLimit=true".

11.2.2. Heap limits

Another option is to automatically stop recording when a certain heap limit in megabytes is reached.

This can also be set at startup via the JVM option "-Dflowstorm.heapLimit=1000", which means stop recording as soon as we used 1000Mb of heap space.

12. Dealing with mutable values

FlowStorm will retain all values pointers when code executes so you can analyze them later. This works great with immutable values but when your code uses mutable values like this :

(let [a (java.util.ArrayList.)]
  (count a)
  (.add a "hello")
  (count a)
  (.add a "world")
  (.add a "!"))

then every time you step over a it will contain the last value ["hello" "world" "!"].

You can fix this situation by extending the flow-storm.runtime.values/SnapshotP protocol like this :

(extend-protocol flow-storm.runtime.values/SnapshotP
  java.util.ArrayList
  (snapshot-value [a] (into [] a)))

to provide FlowStorm a way of creating a snapshot of the mutable value.

Note
ClojureStorm

If you are using ClojureStorm evaluate the previous defmethod in a ns that is not being instrumented to avoid an infinite recursion.

Be aware that this is tricky in multithreading situations, as always with mutable values.

Note
Automatic derefing

FlowStorm will automatically deref Atoms, Refs, Agents, Vars and all pending-realized derefables on tracing so no need to implement flow-storm.runtime.values/snapshot-value for them.

Note
Snapshoting and nested values

Snapshoting only applies to direct references to mutable values. For example if you have an atom inside a nested immutable collection, it will not be snapshoted every time that collection expression is being recorded, because the value being recorded is not a reference to an atom.

If this is important to you, you can still define snapshot-value for clojure.lang.PersistentArrayMap, etc, and walk it down snapshoting everything mutable inside.

Note
snapshot-value and memory footprint

Although snapshot-value was created as a way to deal with mutable values it can be used to replace any value by another in the recordings, which can be useful in other situations like reducing memory footprint when you don’t need the entire value to be recorded.

13. Controlling instrumentation

If you are using ClojureStorm or ClojureScriptStorm it is important to learn how to control what gets instrumented and how to uninstrument things. You can configure what gets instrumented automatically on startup via JVM properties but also change this while your repl is running without the need to restart it.

FlowStorm by default will automatically figure out what to instrument from your project, which you can always disable by setting the -Dclojure.storm.instrumentAutoPrefixes=false.

Note
How are auto prefixes calculated?

When the process starts it will scan all source folders on the classpath (everything not inside a jar containing clojure files) and build a set of all top level namespace. All namespaces under those will be instrumented. Currently it doesn’t detect single level namespaces, like when you have src/core.clj, if this is your case use instrumentOnlyPrefixes.

If you prefer to be explicit about what gets instrumented you can use the JVM property "-Dclojure.storm.instrumentOnlyPrefixes=YOUR_INSTRUMENTATION_STRING"

where YOUR_INSTRUMENTATION_STRING should be a comma separated list of namespaces prefixes like :

my-project.,lib1.,lib2.core

which means automatically instrument my-project.* (which includes all sub namespaces), all lib1.* and only everything under lib2.core

All this can be changed after without restarting your repl from FlowStorm browser.

13.1. Turning instrumentation on an off

You can turn instrumentation on an off by using the button on the toolbar. Remember that the change of this setting will only be effective on newly compiled code.

13.2. Setup startup instrumentation

The first important thing is to setup your instrumentation correctly via JVM properties :

On ClojureStorm :

-Dclojure.storm.instrumentOnlyPrefixes=my-app,my-lib
-Dclojure.storm.instrumentSkipPrefixes=my-app.too-heavy,my-lib.uninteresting
-Dclojure.storm.instrumentSkipRegex=.*test.*

On ClojureScriptStorm :

-Dcljs.storm.instrumentOnlyPrefixes=my-app,my-lib
-Dcljs.storm.instrumentSkipPrefixes=my-app.too-heavy,my-lib.uninteresting

Apart from instrumentOnlyPrefixes which you probably already know, there is instrumentSkipPrefixes which also accepts a comma separated list of namespaces prefixes to skip, and instrumentSkipRegex with accepts a regex for namespaces to skip. All these together allows you to instrument you whole app but some undesired namespaces.

The next important thing is to be able to enable/disable instrumentation and add/remove prefixes without restarting the repl.

13.3. Modifying instrumentation with the Browser

You can use the Browser tool to check and change on the fly the prefixes you configured in the previous section.

browser storm instrumentation 1

Right clicking any namespace will give you options for what level of a namespace you want to instrument.

On the bottom pane (instrumentations) you will see your current instrumentation configuration (if any). Here we can see that everything under ring.middleware.anti-forgery will be instrumented every time something inside it gets compiled.

You can remove entries using the del buttons or temporarily disable/enable them using the Enable all checkbox.

browser storm instrumentation 2

You can use the Add menu in the picure above to add instrumentation prefixes.

After changing any prefix FlowStorm will ask if you want it to reload the affected namespaces for you. Namespace reloading will all reload all namespaces it depends on in topological order, so it shouldn’t break your system in any way.

browser storm instrumentation 3

You can also provide functions to be called before and after reloading in case you need to stop and start your system with :

(flow-storm.api/set-before-reload-callback! (fn [] (println "Before reloading")))
(flow-storm.api/set-after-reload-callback!  (fn [] (println "After reloading")))
Note
Instrumentation

Just changing the prefixes without reloading will not make your currently loaded code [un]instrumented. If you haven’t let FlowStorm reload them for you, you can always recompile them as usual with your editor commands or by executing something like (require 'the-selected.namespace :reload).

13.4. Instrumentation in Vanilla FlowStorm

Note
ClojureStorm

Instructions here only apply to vanilla FlowStorm. If you are using ClojureStorm or ClojureScriptStorm (recommended) this is done automatically for you, so just skip this section.

Code instrumentation in FlowStorm is done by rewriting your code, in a way that doesn’t change its behavior but when executed will trace everything the code is doing.

13.4.1. Instrument any form with #trace

You can instrument any top level form at the repl by writing #trace before it, like this :

#trace
(defn sum [a b]
  (+ a b))

and then evaluating the form.

important

#trace is meant to be used with forms that don’t run immediately, like: defn, defmethod, extend-type, etc. Use #rtrace to trace and run a form, like #rtrace (map inc (range 10)).

13.4.2. Run code with #rtrace

#rtrace is useful in two situations :

First, when instrumenting and running a simple form at the repl, like:

#rtrace (-> (range) (filter odd?) (take 10) (reduce +))

13.4.3. Instrument namespaces

FlowStorm allows you to instrument entire namespaces by providing flow-storm.api/instrument-namespaces-clj.

You call it like this :

(instrument-namespaces-clj #{"org.my-app.core" "cljs."})

The first argument is a set of namespaces prefixes to instrument. In the previous example it means instrument all namespaces starting with org.my-app.core, and all starting with cljs.

The second argument can be a map supporting the following options :

  • :excluding-ns a set of strings with namespaces that should be excluded

  • :disable a set containing any of #{:expr :binding :anonymous-fn} useful for disabling unnecessary traces in code that generate too many

  • :verbose? when true show more logging

13.4.4. What can’t be instrumented?

These are some limitations when instrumenting forms :

  1. Very big forms can’t be fully instrumented. The JVM spec has a limit on the size of methods and instrumentation adds a lot of code. When instrumenting entire namespaces, if you hit this limit on a form a warning will printed on the console saying Instrumented expression is too large for the Clojure compiler and FlowStorm automatically tries to instrument it with a lighter profile, by disabling some instrumentation.

  2. Functions that call recur without a loop

  3. Functions that return recursive lazy sequences. Like (fn foo [] (lazy-seq (…​ (foo))))

13.4.5. Un-instrument code

Un-instrumenting code that has been instrumented with #trace or #ctrace is straight forward, just remove the tag and re evaluate the form.

To un-instrument entire namespaces you can use flow-storm.api/uninstrument-namespaces-clj which accept a set of namespaces prefixes.

13.4.6. Instrument with the browser

Most of the time you can instrument code by just clicking in the browser. The only exceptions are functions that were just defined in the repl and weren’t loaded from a file.

Instrument vars

Using the browser you can navigate to the var you are interested in and then use the instrument button to instrument it.

browser var instrumentation

There are two ways of instrumenting a var :

  • Instrument (instrument just the var source code)

  • Instrument recursively (recursively instrument the var and all vars referred by it)

Instrument namespaces

Using the browser you can also instrument multiple namespaces. Do this by selecting the namespaces you are interested in and then a right click should show you a menu with two instrumentation commands.

browser ns instrumentation
  • Instrument namespace :light - record function arguments and return values (not expressions, no bindings tracing)

  • Instrument namespace :full fully instrument everything

Light instrumentation is useful when you know the functions generate too many traces, so you can opt to trace just functions calls and returns. You can then fully instrument whatever functions you are interested in.

Un-instrument code

The bottom panel shows all instrumented vars and namespaces.

browser uninstrument

You can un-instrument them temporarily with the enable/disable checkbox or permanently with the del button.

Fully instrument a form from the code view
fully instrument form

If you have instrumented a form with the :light profile you can fully instrument it by right clicking on the current form and then clicking Fully instrument this form.

14. Plugins

FlowStorm plugins allows you to add specialized tools to visualize and interact with your recordings.

14.1. Using plugins

For using a pluggin follow each plugging instructions which should normally consists of adding its dependency and then setting the jvm prop flowstorm.plugins.namespaces with all the main namespaces of the plugins you want loaded at startup, like : "-Dflowstorm.plugins.namespaces=flow-storm.plugins.my-plugin.all"

After that you should see a new vertical tab with the plugin UI as you can see here :

plugin demo

14.2. Creating plugins

Creating a pluging consists of two parts :

  • The runtime code that will analyze the recordings and expose an api for the UI

  • The UI component which will visualize and interact with the data via the runtime api

This split is not required, but it is important if you want your plugin to support ClojureScript also or remote Clojure debugging where the UI is not running in the same process as the runtime.

This components are normally split in two files, a runtime.clj and ui.clj, but you can name them however you want.

We are going to go over each part in more detail but for a real plugin please checkout the core.async.flow plugin.

14.2.1. Runtime

Here is a runtime file template you can use :

(ns flow-storm.plugins.my-plugin.runtime
  (:require [flow-storm.runtime.indexes.api :as ia]
            [flow-storm.runtime.debuggers-api :as dbg-api]))

(defn my-data-extraction [flow-id thread-id]
  (let [timeline (ia/get-timeline flow-id thread-id)]
    (reduce (fn [acc tl-entry]
              ;; extract some interesting info from the timeline
              )
            {}
            timeline)
    ))

;; Expose your function so it can be called from the UI part
(dbg-api/register-api-function :plugins.my-plugin/extract-data my-data-extraction)

14.2.2. UI

Here is a ui file template you can use :

(ns flow-storm.plugins.my-plugin.ui
  (:require [flow-storm.debugger.ui.plugins :as fs-plugins]
            [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]])
  (:import [javafx.scene.control Label]))

(fs-plugins/register-plugin
 :my-plugin
 {:label "My plugin"
  :css-resource  "flow-storm-my-plugin/dark.css"
  :dark-css-resource  "flow-storm-my-plugin/dark.css"
  :light-css-resource "flow-storm-my-plugin/light.css"
  :on-focus (fn [{:keys [some-other-data]}]
              ;; This gets called everytime the plugin tab gets focused
              )
  :on-create (fn [_]
               {:fx/node (Label.
                          ;; You can call your runtime registered function
                          (str (runtime-api/call-by-fn-key rt-api :plugins.my-plugin/extract-data [0 10])))
                :some-other-data 42})
  :on-flow-clear (fn [flow-id {:keys [some-other-data]}]
                     ;; this gets called everytime a flow is discarded so you can update your plugin UI accordignly
                   )
  })
Styling plugins UIs

As you saw in the ui plugin registration, you can provide three resources related to styling :

  • :css-resource If there is any, it will be loaded and applied. Here is where you put your JavaFX pluging styles

  • :dark-css-resource This styles are going to be applied only in dark mode

  • :light-css-resource This styles are going to be applied only in light mode

For making sure you plugin styles doesn’t mix with other styles, your plugin is automatically wrapped in a pane with your plugin key (my-plugin in the example above) as a class.

This means your plugin css can contain code like :

.my-plugin .table-view {
    -fx-font-family: 'monospaced';
}

15. JVM options list

This section only collects the options, search for them in the User’s guide for more context and possible values.

15.1. Clojure and ClojureScript

  • -Dflowstorm.startRecording=false

  • -Dflowstorm.plugins.namespaces[.+]=ns1,ns2

  • -Dflowstorm.threadFnCallLimits=org.my-app/fn1:2,org.my-app/fn2:4

  • -Dflowstorm.title=FlowStormMainDebugger

  • -Dflowstorm.theme=dark

  • -Dflowstorm.styles=~/.flow-storm/big-fonts.css

  • -Dflowstorm.fileEditorCommand=emacsclient -n +<<LINE>>:0 <<FILE>>

  • -Dflowstorm.jarEditorCommand=emacsclient -n +<<LINE>>:0 <<JAR>>/<<FILE>>

  • -Dflowstorm.threadTraceLimit=1000

  • -Dflowstorm.throwOnLimit=true

  • -Dflowstorm.autoUpdateUI=false

  • -Dflowstorm.callTreeUpdate=false

  • -Dflowstorm.uiTimeoutMillis=4000

15.2. Only Clojure

  • -Dclojure.storm.instrumentEnable=true

  • -Dclojure.storm.instrumentOnlyPrefixes[.*]=ns-prefix1,ns-prefix2

  • -Dclojure.storm.instrumentAutoPrefixes=false

  • -Dclojure.storm.instrumentSkipPrefixes[.*]=my-app.too-heavy,my-lib.uninteresting

  • -Dclojure.storm.instrumentSkipRegex=.*test.*

  • -Dflowstorm.heapLimit=1000

15.3. Only ClojureScript

  • -Dcljs.storm.instrumentEnable=true

  • -Dcljs.storm.instrumentOnlyPrefixes=ns-prefix1,ns-prefix2

  • -Dcljs.storm.instrumentAutoPrefixes=false

  • -Dcljs.storm.instrumentOnlyPrefixes=my-app,my-lib

  • -Dcljs.storm.instrumentSkipPrefixes=my-app.too-heavy,my-lib.uninteresting

16. Styling and theming

All functions that start the debugger ui (flow-storm.api/local-connect, flow-storm.debugger.main/start-debugger) accept a map with the :styles, :title and :theme keywords. If :styles points to a css file it will be used to overwrite the default styles, in case you want to change colors, make your fonts bigger, etc. :theme could be one of :auto (default), :light, :dark. Title can be used to distinguish between multiple debugger instances.

Like this :

user> (local-connect {:styles "~/.flow-storm/big-fonts.css", :theme :dark, :title "FlowStormMainDebugger"})

If you are using ClojureStorm you can also provide them with :

-Dflowstorm.title=FlowStormMainDebugger
-Dflowstorm.theme=dark
-Dflowstorm.styles=~/.flow-storm/big-fonts.css

17. Controlling logging

FlowStorm uses JUL (java.util.logging) as the loggging library.

You can configure JUL logging by starting your repl with -Djava.util.logging.config.file=./logging.properties

If you need to disable logging you can put this in your logging.properties file :

handlers = java.util.logging.ConsoleHandler

flow_storm.level = SEVERE
clojure.storm.level = SEVERE

18. Key bindings

18.1. General

  • Ctrl-g Cancel any long running task (only search supported yet)

  • Ctrl-l Clean all debugger state

  • Ctrl-d Toggle debug-mode. Will log useful debugging information to the console

  • Ctrl-u Unblock all breakpoint blocked threads if any

  • Ctrl-t Rotate themes

  • Ctrl-plus Increment font size

  • Ctrl-minus Decrement font size

  • F "Select the Flows tool"

  • B "Select the Browser tool"

  • T "Select the Taps tool"

  • D "Select the Docs tool"

18.2. Flows

  • 0-9 Open focus flow-N threads menu, N being the pressed key

  • t Select the tree tool (needs to be inside a thread)

  • c Select the code tool (needs to be inside a thread)

  • f Select the functions tool (needs to be inside a thread)

  • P Step prev over. Go to previous step on the same frame

  • p Step prev

  • n Step next

  • N Step next over. Go to next step on the same frame

  • ^ Step out

  • < Step first

  • > Step last

  • Ctrl-f Copy current function symbol

  • Ctrl-Shift-f Copy current function call form

  • Ctrl-z Undo navigation

  • Ctrl-r Redo navigation

19. Debugging react native applications

Debugging ClojureScript react native application needs a combination of ClojureScript and remote debugging.

Assuming you are using shadow-cljs, have added the flow-storm-inst dependency, and that it started a nrepl server on port 9000, you can start a debugger and connect to it by running :

clj -Sforce -Sdeps '{:deps {com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}}' -X flow-storm.debugger.main/start-debugger :port 9000 :repl-type :shadow :build-id :your-app-build-id :debugger-host '"YOUR_DEV_MACHINE_IP"'

You also need to make it possible for the device to connect back to the debugger on port 7722. You can accomplish this by running :

adb reverse tcp:7722 tcp:7722

Also remember that you need to have installed the websocket npm library. You can do this like :

npm install websocket --save

20. Working on Windows with WSL2

For those using current versions of WSL2 on Windows it should be pretty straight forward.

  • export DISPLAY=:0

  • export WSL2_GUI_APPS_ENABLED=1

Font issues had been reported on some distros, like java.lang.NullPointerException: Cannot read field "firstFont" because "<local4>" is null which seams to be solved just by installing font packages like dejavu-fonts or ttf-dejavu depending on the distro.

21. Opening forms in editors

You can add this two jvm options to tell FlowStorm how to open forms in files and inside jars :

  • flowstorm.jarEditorCommand : a command with optional <<JAR>>, <<FILE>> and <<LINE>> placeholders

  • flowstorm.fileEditorCommand : a command with optional <<FILE>> and <<LINE>> placeholders

If you define those, clicking on your forms namespaces link in the code tool should run the provided commands. On expressions sub-forms that contains line meta you should also be able to right click and select "Open in editor" which should open the file at that specific line (useful for long forms).

Here are some known setups for most common editors :

21.1. Emacs

;; for opening your project files
"-Dflowstorm.fileEditorCommand=emacsclient -n +<<LINE>>:0 <<FILE>>"

;; simple way for opening files inside jars (works on linux only)
"-Dflowstorm.jarEditorCommand=emacsclient -n +<<LINE>>:0 <<JAR>>/<<FILE>>"

;; for opening files inside jars that works on every OS (requires FlowStorm >= 3.17.3)
"-Dflowstorm.jarEditorCommand=emacsclient --eval '(let ((b (cider-find-file \"jar:file:<<JAR>>!/<<FILE>>\"))) (with-current-buffer b (switch-to-buffer b) (goto-char (point-min)) (forward-line (1- <<LINE>>))))'"

21.2. VSCode

"-Dflowstorm.fileEditorCommand=code --goto <<FILE>>:<<LINE>>"

21.3. IntelliJ

"-Dflowstorm.fileEditorCommand=idea --line <<LINE>> <<FILE>>"

21.4. Vim

"-Dflowstorm.fileEditorCommand=vim +<<LINE>> <<FILE>>"

22. Editors/IDEs integration

22.1. Emacs

Checkout Cider Storm an Emacs Cider front-end with support for Clojure and ClojureScript.

22.2. VSCode

With the following alias setup in deps.edn:

{:aliases {:flowstorm {:classpath-overrides {org.clojure/clojure nil}
                       :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.4"}
                                    com.github.flow-storm/flow-storm-dbg {:mvn/version "4.5.9"}}
                       :jvm-opts ["-Dflowstorm.startRecording=true"
                                  "-Dclojure.storm.intrumentEnable=true"
                                  "-Dclojure.storm.intrumentAutoPrefixes=true"]}}}

Create a custom connect sequence in the VSCode settings.json:

{
      "name": "flowstorm",
      "projectType": "deps.edn",
      "cljsType": "none",
      "extraNReplMiddleware": ["flow-storm.nrepl.middleware/wrap-flow-storm"],
      "afterCLJReplJackInCode": "((requiring-resolve 'flow-storm.storm-api/start-debugger))",
      "menuSelections": {
        "cljAliases": ["flowstorm"]
      }
}

Jack-in using the flowstorm sequence from the menu.

24. Known limitations and issues

24.1. Code with macros that don’t preserve meta

FlowStorm works fine with most macros, except the ones that don’t preserve meta at macroexpansion, which FlowStorm needs in their absence, can cause GAPS IN EXECUTION TRACING AND PREVENT IT FROM LINKING THE EXECUTED CODE BACK TO THE ORIGINAL SOURCE.

When macros are involved, the forms compiled by the Clojure compiler aren’t the ones on your source files, but the ones generated by macro-expanding all the macros. In order to link the compiled forms back to the forms in your source code, for each instrumented form, right after the form is read by the reader, FlowStorm will walk the form down, annotating with meta each sub-form with a coordinate, which will then be used after macro expansion to link a compiled expression back to your source code.

Macros can be as simple as code reorganizing ones (like , when, and, defn, etc) or whole compilers like clojure.core.async/go and Electric, so it really depends on the macros.

If you see code inside a macro not being traced feel free to report an issue, there is nothing FlowStorm can do from its side but we can work together with the macro developer making sure it preserves all meta after macro expansion, which sometimes may be possible.

24.2. Locals pane and mutable values

The Locals pane will show the value of each binding for a symbol AT BINDING TIME, which is the same thing no matter where you are in the current block when working with immutable objects, BUT NOT WHEN WORKING WITH MUTABLE ONES.

If what was bound was mutable in any way, you will be seeing the value at binding time, and not at current time which could cause some confusion.

24.3. Closed over bindings aren’t displayed in Locals

The locals pane will only display bindings for the current function. Locals visible from the current function but not defined in it (like in the case of closures) aren’t shown.

24.4. Don’t instrument clojure.core or FlowStorm itself

Currently we can’t instrument clojure.core or FlowStorm itself since they endup in infinite tracing recursions. This can be solved, but it is currently a limitation.

24.5. IF test expressions tracing with intrinsics

When you have code like :

(defn foo [^long l]
  (if (zero? l)
    (+ l 1)
    (+ l 2)))

because l is a primitive long, the compiler can replace the (zero? l) with intrinsics (LCONST_0, LCMP, IFNE) so the (zero? l) isn’t a expression anymore, just a statement. In these cases you will see the if test return un-highlighted, but you can still tell which branch the code went thru because the chosen branch will be the highlighted one.

24.6. Loading an entire file while recording records some weird code

This is because most editors, (specially via nrepl) eval some other code on your namespace in order to load the contents of your file. If that namespace is instrumented this will be also recorded, even when it is probably not of your interest. This is harmless, just clear your recordings before running and recording anything else.

If you follow the best practice of start recording right before running the stuff you are interested in recording you should never see this.

24.7. Macro calls tracing code you don’t see on your code

When you are evaluating some code that macroexpands to a (do form-1 …​ form-n) the compiler recursively calls eval on the sub forms. Because it is tricky in the compiler to tell apart your original source form from the ones the macroexpansion returned those form-1 to form-n get instrumented and then traced as if they were on your code.

The tricky part is related to tooling like IDEs sometimes wrapping your forms in some macros that expand to a (do form-1 …​ form-n) so we can’t simply stop instrumenting after that situation.

25. Troubleshooting

25.1. The outputs panel doesn’t work

Checkout that you don’t have piggieback on the classpath dragged by some dependency. Currently if piggieback is pressent FlowStorm will assume a ClojureScript repl in which the outputs panel isn’t supported yet.

25.2. Run with JDK 11

FlowStorm UI requires JDK >= 17. If you can’t upgrade your JDK you can still use it by downgrading JavaFx.

If that is the case add these dependencies to your alias :

org.openjfx/javafx-controls {:mvn/version "19.0.2"}
org.openjfx/javafx-base     {:mvn/version "19.0.2"}
org.openjfx/javafx-graphics {:mvn/version "19.0.2"}
org.openjfx/javafx-web      {:mvn/version "19.0.2"}
================================================ FILE: examples/plugins/basic-plugin/deps.edn ================================================ {} ================================================ FILE: examples/plugins/basic-plugin/src/flow_storm/plugins/timelines_counters/all.clj ================================================ (ns flow-storm.plugins.timelines-counters.all (:require [flow-storm.plugins.timelines-counters.ui] [flow-storm.plugins.timelines-counters.runtime])) ================================================ FILE: examples/plugins/basic-plugin/src/flow_storm/plugins/timelines_counters/runtime.cljc ================================================ (ns flow-storm.plugins.timelines-counters.runtime (:require [flow-storm.runtime.indexes.api :as ia] [flow-storm.runtime.debuggers-api :as dbg-api])) (defn timelines-counts [flow-id] (reduce (fn [acc [fid tid]] (if (= flow-id fid) (let [timeline (ia/get-timeline flow-id tid)] (assoc acc tid (count timeline))) acc)) {} (ia/all-threads))) (dbg-api/register-api-function :plugins.timelines-counters/timelines-counts timelines-counts) ================================================ FILE: examples/plugins/basic-plugin/src/flow_storm/plugins/timelines_counters/ui.clj ================================================ (ns flow-storm.plugins.timelines-counters.ui (:require [flow-storm.debugger.ui.plugins :as fs-plugins] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [clojure.string :as str]) (:import [javafx.scene.control Label Button TextField] [javafx.scene.layout HBox VBox] [javafx.event EventHandler] [javafx.scene Node])) (fs-plugins/register-plugin :timelines-counter {:label "Timelines counter" :on-create (fn [_] (let [counts-lbl (Label. "") flow-id-txt (TextField. "0") refresh-btn (doto (Button. "Refresh") (.setOnAction (reify javafx.event.EventHandler (handle [_ _] (->> (runtime-api/call-by-fn-key rt-api :plugins.timelines-counters/timelines-counts [(parse-long (.getText flow-id-txt))]) (mapv (fn [[thread-id cnt]] (format "ThreadId: %d, Timeline Count: %d" thread-id cnt))) (str/join "\n") (.setText counts-lbl)))))) tools-box (HBox. (into-array Node [(Label. "FlowId:") flow-id-txt refresh-btn])) main-box (VBox. (into-array Node [tools-box counts-lbl]))] {:fx/node main-box}))}) ================================================ FILE: llm-prompt.txt ================================================ You can use the FlowStorm debugger tools via the repl to analyze FlowStorm recordings which records Clojure systems execution, for debugging or gathering extra information that can only be known at runtime. Recordings are grouped into flows. A flow just groups related recordings of some system's execution. Each flow contains a group of timelines, which are arrays containig the recorded executions steps for a thread. On each recorded flow there is one timeline per thread. If the recorded program executed in multiple threads, each of the timelines can be accesed by flow-id and thread-id. Each timeline entry is a Clojure map of one of the following :type : - :fn-call A entry representing a function call - :fn-return A entry representing a function return - :fn-unwind A entry representing a function that throwed and exception instead of returning - :expr-exec A entry representing a expression execution. Next I'll list all the functions you have available for retrieving and exploring the timlines. They allow you to write small Clojure programs to inspect the timelines for debugging or understanding anything about a Clojure program's execution. You can get all the recorded flows with their threads ids by calling : (flow-storm.runtime.indexes.api/all-flows) which will return a Clojure map from flow-id to a vector of threads ids. For each `thread-id` you can retrieve its timeline calling : (flow-storm.runtime.indexes.api/get-referenced-maps-timeline flow-id thread-id) You can bound each timeline to a name with `def` once and then work with them. Since timelines implement most Clojure collections interfaces, you can call get, count, map, take, filter, reduce, etc on them. For example, assuming you have bound a timeline to `tl`, you can get the entry at position 5 on the timeline by evaluating : (get tl 5) If the entry is of `:fn-call` type it will contain the following keys : - :type Will be :fn-call - :fn-name A string containing the name of the function being called - :fn-ns A string containing the namespace of the function being called - :form-id The id to the Clojure function form - :fn-args-ref The value-id of the function arguments vector - :parent-idx The timeline index of the parent function, or nil if this is the first funciton being called - :ret-idx The timeline index of the fn-return step for this function call If the entry is of `:expr` type it will contain the following keys : - :type Will be :expr - :result-ref The value-id of this expression value - :fn-call-idx The index of the fn-call wrapping this step - :form-id The id to the Clojure function form - :coord The coordinate of this expression in the form represented by form-id If the entry is of `:fn-return` type it will contain the following keys : - :type Will be :fn-return - :result-ref The value-id of this expression value - :fn-call-idx The index of the fn-call wrapping this step - :form-id The id to the Clojure function form - :coord The coordinate of this expression in the form represented by form-id If the entry is of `:fn-unwind` type it will contain the following keys : - :type Will be :fn-unwind - :throwable-ref The value-id of the Throwable/Exception object - :form-id The id to the Clojure function form - :coord The coordinate of this expression in the form represented by form-id Using the :fn-call (:parent-idx, :ret-idx), and the :fn-call-idx of the other entries you can also navigate the timeline as a graph. You can call `flow-storm.runtime.indexes.api/get-form-at-coord` with a form-id and a coordinate to retrieve the Clojure form for a specific :form-id and :coord. You can use a nil coord to retrieve the outer form. You can call `flow-storm.runtime.values/deref-val-id` with a value-id to get the Clojure value represented by that value id. So for example given a value-id of 1, you can preview it with something like : (binding [*print-level* 5 *print-length* 5] (flow-storm.plugins.mcp.runtime/deref-val-id 1)) but you coan inspect the value however you need. ================================================ FILE: package.json ================================================ { "dependencies": { "websocket": "1.0.34", "shadow-cljs": "2.23.3" } } ================================================ FILE: resources/flowstorm/fonts/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: resources/flowstorm/styles/font-size-lg.css ================================================ .root {-fx-font-size: 19;} .ikonli-font-icon {-fx-font-size: 21;} .btn-sm {-fx-font-size: 16;} .btn-xs {-fx-font-size: 13;} .threads-tab-pane > .tab-header-area {-fx-padding: 5 0 0 158;} .flows-tab-pane > .tab-header-area {-fx-padding: 10 0 0 370;} .flows-combo {-fx-pref-width: 184; -fx-font-size: 18;} ================================================ FILE: resources/flowstorm/styles/font-size-md.css ================================================ .root {-fx-font-size: 16;} .ikonli-font-icon {-fx-font-size: 17;} .btn-sm {-fx-font-size: 13;} .btn-xs {-fx-font-size: 11;} .threads-tab-pane > .tab-header-area {-fx-padding: 5 0 0 134;} .flows-tab-pane > .tab-header-area {-fx-padding: 10 0 0 315 ;} .flows-combo {-fx-pref-width: 155; -fx-font-size: 15;} ================================================ FILE: resources/flowstorm/styles/font-size-sm.css ================================================ .root {-fx-font-size: 13;} .ikonli-font-icon {-fx-font-size: 15;} .btn-sm {-fx-font-size: 10;} .btn-xs {-fx-font-size: 8;} .threads-tab-pane > .tab-header-area {-fx-padding: 5 0 0 112 ;} .flows-tab-pane > .tab-header-area {-fx-padding: 10 0 0 265 ;} .flows-combo {-fx-pref-width: 127; -fx-font-size: 13;} ================================================ FILE: resources/flowstorm/styles/font-size-xl.css ================================================ .root {-fx-font-size: 22;} .ikonli-font-icon {-fx-font-size: 23;} .btn-sm {-fx-font-size: 19;} .btn-xs {-fx-font-size: 16;} .threads-tab-pane > .tab-header-area {-fx-padding: 5 0 0 180;} .flows-tab-pane > .tab-header-area {-fx-padding: 10 0 0 420;} .flows-combo {-fx-pref-width: 211; -fx-font-size: 21;} ================================================ FILE: resources/flowstorm/styles/styles.css ================================================ @font-face { src: url('../fonts/Roboto-Light.ttf'); } @font-face { src: url('../fonts/Roboto-Medium.ttf'); } .main-pane { -fx-font-family: 'Roboto Medium'; } .context-menu { -fx-background-color: -fx-theme-base4; } .separator-label { -fx-text-fill: -fx-theme-separator; } .tab .ikonli-font-icon { -fx-icon-color: -fx-theme-button2-text; -fx-background-color: linear-gradient(to top, -fx-base, derive(-fx-base,30%)); } .button { -fx-background-color: -fx-theme-button1-background; -fx-text-fill: -fx-theme-button1-text; } .toggle-button:selected { -fx-background-color: -fx-theme-on; -fx-text-fill: #323232; } .list-view .list-cell:selected { -fx-background-color: -fx-theme-list-selection; -fx-text-fill: -fx-theme-flow-code-text; } .table-view .table-row-cell:selected { -fx-background-color: -fx-theme-list-selection; -fx-text-fill: -fx-theme-flow-code-text; } .combo-box .label { -fx-text-fill: -fx-theme-button2-text; } .tab { -fx-opacity: 0.5; -fx-text-fill: -fx-theme-button1-text; } .hl-combo { -fx-background-color: -fx-theme-hl-combos; } .important-combo { -fx-background-color: -fx-theme-important-combos; } .tab: selected { -fx-opacity: 1; } .vertical-tab:selected { -fx-background-color: -fx-theme-selected-main-tools-tab-background; } .vertical-tab:selected .tab-label { -fx-text-fill: -fx-theme-selected-main-tools-tab-text; } .button.thread-continue-btn { -fx-background-color: -fx-theme-breakpoint-continue; } .thread-blocked { -fx-text-fill: -fx-theme-warning; } .button.clear-break-btn { -fx-background-color: -fx-theme-breakpoint; } .link-lbl { -fx-text-fill: -fx-theme-links; -fx-cursor: hand; } .link-lbl-no-color { -fx-underline: true; -fx-cursor: hand; } .light { -fx-opacity: 0.3; } .button:hover { -fx-background-color: -fx-theme-button-hover; -fx-color: -fx-hover-base; } .button:focused { /* this is so buttons doesn't look smaller when they are focused */ -fx-background-insets: 0 0 -1 0; } .button .ikonli-font-icon { -fx-icon-color: -fx-theme-button1-text; } .thread-refresh {-fx-background-color: -fx-theme-base1;} .thread-refresh .ikonli-font-icon {-fx-icon-color: red;} .mirrored { -fx-scale-x: -1; } .text-field { -fx-prompt-text-fill: -fx-theme-button2-text; } .ok { -fx-background-color: -fx-theme-ok; -fx-text-fill: #323232; } .warning { -fx-background-color: -fx-theme-warning; -fx-text-fill: #323232; } .fail { -fx-background-color: -fx-theme-attention; -fx-text-fill: #323232; } .attention { -fx-background-color: -fx-theme-attention; -fx-text-fill: #323232; } .tree-search { -fx-background-insets: 1 0 1 0; } .form-pane { -fx-background-color: -fx-theme-base2; } .dialog-pane { -fx-background-color: -fx-theme-base2; } .form-pane.form-background-highlighted { -fx-background-color: -fx-theme-form-highlight; } .label.defmethod { -fx-text-fill: -fx-theme-defmethod-text; } .label.defn { -fx-text-fill: -fx-theme-defn-text; } .label.extend-type { -fx-text-fill: -fx-theme-extend-text; } .label.extend-protocol { -fx-text-fill: -fx-theme-extend-text; } .label.anonymous { -fx-text-fill: -fx-theme-defn-text; } .label.fn-ns { -fx-text-fill: -fx-theme-dim-text; } .code-token { -fx-font-family: 'monospaced'; -fx-fill: -fx-theme-flow-code-text; } .monospaced { -fx-font-family: 'monospaced'; } .code-token.executing { -rtfx-background-color: -fx-theme-flow-code-executing-background; -fx-fill: -fx-theme-flow-code-executing-text; } .code-token.executing-dim { -rtfx-background-color: -fx-theme-flow-code-executing-background-dim; } .code-token.interesting { -fx-fill: -fx-theme-flow-code-interesting-text; -fx-cursor: hand; -fx-font-weight: bold; } .code-token.possible { -fx-cursor: default; } .forms-scroll-container { -fx-padding: 10 0 0 0; } .form-pane { -fx-padding: 10; } .thread-controls-pane { -fx-background-color: -fx-theme-base2; -fx-padding: 10; } .trace-position-box { -fx-alignment: center; } .threads-tab-pane > .tab-header-area { -fx-padding: 5 0 0 92 ; } .flows-tab-pane > .tab-header-area { -fx-padding: 5 0 0 135 ; } .fn-call-list-cell { -fx-background-color: -fx-theme-base2; } .fn-call-list-cell .label{ -fx-text-fill: -fx-theme-flow-code-text; } /*****************/ /* Browser stuff */ /*****************/ .label.browser-fn-fq-name { -fx-font-weight: bold; -fx-text-fill: -fx-theme-button1-background; -fx-padding: 10; } .browser-fn-args-box { -fx-padding: 10; } .label.browser-fn-attr { -fx-padding: 10; } .button.browser-instrument-btn { -fx-display: none; visibility: hidden; } .button.browser-break-btn { visibility: hidden; -fx-background-color: -fx-theme-breakpoint; } .button.browser-break-btn:hover { visibility: hidden; -fx-background-color: -fx-theme-button-hover; -fx-color: -fx-hover-base; } .button.browser-break-btn.enable { visibility: visible; } .button.browser-instrument-btn.enable { visibility: visible; } .browser-var-buttons { -fx-padding: 10; } .browser-instr-type-lbl { -fx-text-fill: -fx-theme-dim-text; } .browser-instr-tools-box { -fx-padding: 10; } .main-bottom-bar-box { -fx-background-color: -fx-theme-base1; } .button.reload-tree-btn { -fx-background-color: -fx-theme-button2-background; } /****************/ /* Docs browser */ /****************/ .docs-fn-name { -fx-font-weight: bold; -fx-text-fill: -fx-theme-title-label; -fx-padding: 5; } .docs-label { -fx-text-fill: -fx-theme-field-label; -fx-padding: 5; } .docs-example-box { -fx-padding: 10; } .docs-box { -fx-padding: 10; -fx-background-color: -fx-theme-base2; } .docs-type-name { -fx-text-fill: -fx-theme-extend-text; } .docs-arg-symbol { -fx-text-fill: -fx-theme-dim-text; } .hidden-node { -fx-display: none; visibility: hidden; -fx-max-width: 0; } .docs-example-ret-symbol { -fx-text-fill: -fx-theme-links; } /*****************/ /* Timeline tool */ /*****************/ .timeline-tool .controls-box { -fx-padding : 10; } /****************/ /* Data Windows */ /****************/ .data-window .breadcrums .button { -fx-background-color: -fx-theme-breadcrums; } /***********/ /* Outputs */ /***********/ .outputs-dw { -fx-background-color: -fx-theme-base2; } ================================================ FILE: resources/flowstorm/styles/theme_dark.css ================================================ * { -fx-theme-base1: #323232; -fx-theme-base2: #3f474f; -fx-theme-base3: #2f353b; -fx-theme-base4: #717175; -fx-theme-separator: orange; -fx-theme-dim-text: #919191; -fx-theme-button1-background: #336699; -fx-theme-button1-text: white; -fx-theme-button-hover: #9c0084; -fx-theme-button2-background: #919191; -fx-theme-button2-text: white; -fx-theme-selected-main-tools-tab-background: #4743ba; -fx-theme-selected-main-tools-tab-text: white; -fx-theme-flow-code-text: #FFF; -fx-theme-flow-code-executing-text: #3f474f; -fx-theme-flow-code-executing-background: #00d97e; -fx-theme-flow-code-executing-background-dim: #009c5a; -fx-theme-flow-code-interesting-text: #eb34d5; -fx-theme-defmethod-text: #de00c0; -fx-theme-defn-text: #FFF; -fx-theme-extend-text: #65ce8a; -fx-theme-links: #de00c0; -fx-theme-breadcrums: #de00c0; -fx-theme-ok: #00ff00; -fx-theme-on: #00d97e; -fx-theme-attention: #ff0000; -fx-theme-warning: orange; -fx-theme-title-label: orange; -fx-theme-field-label: #de00c0; -fx-theme-breakpoint: #ff0000; -fx-theme-breakpoint-continue: #60d61b; -fx-theme-form-highlight: #636363; -fx-theme-list-selection: #788da1; -fx-theme-hl-combos: #555478; -fx-theme-important-combos: #792a6c; } .root { -fx-base: -fx-theme-base1; -fx-background: -fx-theme-base1; -fx-control-inner-background: -fx-theme-base1; } ================================================ FILE: resources/flowstorm/styles/theme_light.css ================================================ * { -fx-theme-base1: #ddd; -fx-theme-base2: #eee; -fx-theme-base3: #ebecff; -fx-theme-base4: #ebecff; -fx-theme-separator: #919191; -fx-theme-dim-text: #919191; -fx-theme-button1-background: #336699; -fx-theme-button1-text: white; -fx-theme-button-hover: #9c0084; -fx-theme-button2-background: #919191; -fx-theme-button2-text: #0b0d2e; -fx-theme-selected-main-tools-tab-background: #4743ba; -fx-theme-selected-main-tools-tab-text: white; -fx-theme-flow-code-text: #3f474f; -fx-theme-flow-code-executing-text: #3f474f; -fx-theme-flow-code-executing-background: #43e8a4; -fx-theme-flow-code-executing-background-dim: #9de3c6; -fx-theme-flow-code-interesting-text: #eb34d5; -fx-theme-defmethod-text: #9c0084; -fx-theme-defn-text: #222; -fx-theme-extend-text: #369658; -fx-theme-links: #9c0084; -fx-theme-breadcrums: #9c0084; -fx-theme-ok: #00ff00; -fx-theme-on: #00d97e; -fx-theme-attention: #ff0000; -fx-theme-warning: orange; -fx-theme-title-label: #336699; -fx-theme-field-label: #9c0084; -fx-theme-breakpoint: #ff0000; -fx-theme-breakpoint-continue: #369658; -fx-theme-form-highlight: #BBBBBB; -fx-theme-list-selection: #afbbc4; -fx-theme-hl-combos: #b9b8d4; -fx-theme-important-combos: #c9a9c4; } .root { -fx-base: -fx-theme-base1; -fx-background: -fx-theme-base1; -fx-control-inner-background: -fx-theme-base1; } ================================================ FILE: scripts/flow-clj ================================================ # Make a babashka script or shell script to run trampoline like clj -x com.foo/fn args # clj -X:dbg:inst:dev:build flow-storm.api/trampoline :ns-set '#{\"hf.depstar\"}' :fn-symb 'hf.depstar/jar' :fn-args '[{:jar \"flow-storm-dbg.jar\" :aliases [:dbg] :paths-only false :sync-pom true :version \"1.1.1\" :group-id \"jpmonettas\" :artifact-id \"flow-storm-dbg\"}]' ================================================ FILE: scripts/gsettings ================================================ #!/home/jmonetta/bin/bb (ns gsettings) (require '[clojure.tools.cli :refer [parse-opts]]) (let [[first-arg] *command-line-args* get-theme (fn [] (slurp "/home/jmonetta/.flow-storm/mock-current-gtk-theme"))] (cond (= first-arg "get") (println (get-theme)) (= first-arg "monitor") (loop [curr-theme (get-theme) last-reported-theme nil] (if (not= curr-theme last-reported-theme) (do (println (format "xxx %s" curr-theme)) (recur (get-theme) curr-theme)) (do (Thread/sleep 1000) (recur (get-theme) last-reported-theme)))))) ================================================ FILE: scripts/mock-gnome.sh ================================================ #/bin/bash export XDG_CURRENT_DESKTOP=gnome export PATH=/home/jmonetta/my-projects/flow-storm-debugger/scripts/:$PATH bash ================================================ FILE: shadow-cljs.edn ================================================ ;; shadow-cljs configuration {:deps {:aliases [:cljs-storm :dev]} :builds {:dev-test {:target :node-script :main dev/-main :output-to "public/dev-test.js"}} :nrepl {:port 9000 :middleware [refactor-nrepl.middleware/wrap-refactor cider.nrepl/cider-middleware cider.piggieback/wrap-cljs-repl flow-storm.nrepl.middleware/wrap-flow-storm]} } ================================================ FILE: src-dbg/flow_storm/debugger/docs.clj ================================================ (ns flow-storm.debugger.docs (:require [flow-storm.state-management :refer [defstate]] [flow-storm.utils :as utils] [clojure.java.io :as io] [clojure.edn :as edn])) (declare start) (declare stop) (declare fn-docs) (def docs-file-name "flow-docs.edn") (def deprecated-docs-file-name "samples.edn") (defn- classpath-uris [file-name] (let [cl (.. Thread currentThread getContextClassLoader)] (->> (enumeration-seq (.getResources cl file-name)) (map #(.toURI %))))) (defn- read-deprecated-docs [files] (let [fns-data (reduce (fn [r file] (merge r (-> file slurp edn/read-string))) {} files)] {:functions/data fns-data})) (defn- read-docs [files] (when (seq files) (utils/log (format "Loading docs using %s files" (pr-str files)))) (reduce (fn [r file] (merge-with merge r (-> file slurp edn/read-string))) {} files)) (defn read-classpath-docs [] (let [dev-docs-file (io/file docs-file-name) ;; just to make dev easier docs-files (cond-> (classpath-uris docs-file-name) (.exists dev-docs-file) (conj dev-docs-file)) docs-maps (read-docs docs-files) deprecated-docs-maps (read-deprecated-docs (classpath-uris deprecated-docs-file-name))] (merge-with merge docs-maps deprecated-docs-maps))) (defstate fn-docs :start (fn [_] (start)) :stop (fn [] (stop))) (defn start [] (-> (read-classpath-docs) :functions/data)) (defn stop [] nil) ================================================ FILE: src-dbg/flow_storm/debugger/events_processor.clj ================================================ (ns flow-storm.debugger.events-processor "Processing events the debugger receives from the runtime" (:require [flow-storm.debugger.ui.browser.screen :as browser-screen] [flow-storm.debugger.ui.outputs.screen :as outputs-screen] [flow-storm.debugger.ui.main :as ui-main] [flow-storm.debugger.ui.flows.screen :as flows-screen] [flow-storm.debugger.ui.flows.bookmarks :as bookmarks] [flow-storm.debugger.ui.docs.screen :as docs-screen] [flow-storm.debugger.ui.flows.general :as ui-general :refer [show-message]] [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.data-windows.data-windows :as data-windows] [flow-storm.utils :refer [log]] [flow-storm.jobs :refer [timeline-updates-check-interval]] [flow-storm.debugger.state :as dbg-state])) (defn- vanilla-var-instrumented-event [{:keys [var-ns var-name]}] (ui-utils/run-later (browser-screen/add-to-instrumentation-list (browser-screen/make-inst-var var-ns var-name)) (ui-general/select-main-tools-tab "tool-browser"))) (defn- vanilla-var-uninstrumented-event [{:keys [var-ns var-name]}] (ui-utils/run-later (browser-screen/remove-from-instrumentation-list (browser-screen/make-inst-var var-ns var-name)) (ui-general/select-main-tools-tab "tool-browser"))) (defn- vanilla-namespace-instrumented-event [{:keys [ns-name]}] (ui-utils/run-later (browser-screen/add-to-instrumentation-list (browser-screen/make-inst-ns ns-name)) (ui-general/select-main-tools-tab "tool-browser"))) (defn- vanilla-namespace-uninstrumented-event [{:keys [ns-name]}] (ui-utils/run-later (browser-screen/remove-from-instrumentation-list (browser-screen/make-inst-ns ns-name)) (ui-general/select-main-tools-tab "tool-browser"))) (defn- storm-instrumentation-updated-event [data] (ui-utils/run-later (browser-screen/update-storm-instrumentation data) (ui-general/select-main-tools-tab "tool-browser"))) (defn- tap-event [{:keys [value]}] (ui-utils/run-later (outputs-screen/add-tap-value value))) (defn out-write-event [{:keys [msg]}] (ui-utils/run-later (outputs-screen/add-out-write msg))) (defn err-write-event [{:keys [msg]}] (ui-utils/run-later (outputs-screen/add-err-write msg))) (defn last-evals-update-event [{:keys [last-evals-refs]}] (ui-utils/run-later (outputs-screen/update-last-evals last-evals-refs))) (defn- flow-created-event [flow-info] (ui-utils/run-now (ui-main/create-flow flow-info))) (defn- flow-discarded-event [flow-info] (ui-utils/run-now (flows-screen/clear-debugger-flow (:flow-id flow-info)))) (defn- threads-updated-event [{:keys [flow-id flow-threads-info]}] (ui-utils/run-now (flows-screen/update-threads-list flow-id flow-threads-info))) (defn- timeline-updated-event [{:keys [flow-id thread-id]}] ;; If there is a tab open for the thread already, update it (when (dbg-state/get-thread flow-id thread-id) (ui-utils/run-later (if (:auto-update-ui? (dbg-state/debugger-config)) (let [start (System/currentTimeMillis)] (flows-screen/update-outdated-thread-ui flow-id thread-id) (when (> (- (System/currentTimeMillis) start) timeline-updates-check-interval) (log "WARNING: UI updates are slow, disabling automatic updates.") (dbg-state/set-auto-update-ui false))) (flows-screen/make-outdated-thread flow-id thread-id))))) (defn- task-submitted-event [_] (ui-main/set-task-cancel-btn-enable true)) (defn- task-finished-event [_] (ui-main/set-task-cancel-btn-enable false)) (defn- task-failed-event [{:keys [message]}] (ui-main/set-task-cancel-btn-enable false) (ui-general/show-message message :error)) (defn- heap-info-update-event [ev-args-map] (ui-main/update-heap-indicator ev-args-map)) (defn- goto-location-event [loc] (ui-utils/run-now (flows-screen/goto-location loc))) (defn- show-doc-event [{:keys [var-symbol]}] (ui-utils/run-now (ui-general/select-main-tools-tab "tool-docs") (docs-screen/show-doc var-symbol))) (defn- break-installed-event [{:keys [fq-fn-symb]}] (ui-utils/run-later (browser-screen/add-to-instrumentation-list (browser-screen/make-inst-break fq-fn-symb)))) (defn- break-removed-event [{:keys [fq-fn-symb]}] (ui-utils/run-later (browser-screen/remove-from-instrumentation-list (browser-screen/make-inst-break fq-fn-symb)))) (defn- recording-updated-event [{:keys [recording?]}] (flows-screen/set-recording-btn recording?)) (defn- multi-timeline-recording-updated-event [{:keys [recording?]}] (flows-screen/set-multi-timeline-recording-btn recording?)) (defn- function-unwinded-event [{:keys [flow-id] :as unwind-data}] (case (dbg-state/maybe-add-exception unwind-data) :ex-limit-reached (show-message "Too many exceptions throwed, showing only the first ones" :warning) (:ex-skipped :ex-limit-passed) nil :ex-added (ui-utils/run-later (flows-screen/add-exception-to-menu unwind-data) ;; the first time we encounter an exception, navigate to that location (when (and (:auto-jump-on-exception? (dbg-state/debugger-config)) (= 1 (count (dbg-state/flow-exceptions flow-id)))) (flows-screen/goto-location unwind-data))))) (defn expression-bookmark-event [{:keys [flow-id thread-id idx note source] :as bookmark-location}] (ui-utils/run-later (dbg-state/add-bookmark {:flow-id flow-id :thread-id thread-id :idx idx :source (or source :bookmark.source/api) :note note}) (bookmarks/update-bookmarks) ;; jump to the first mark, unless, we've already jumped to an exception (when (and (= 1 (->> (dbg-state/flow-bookmarks flow-id) (filter (fn [{:keys [source]}] (= source :bookmark.source/api))) count)) (not (and (:auto-jump-on-exception? (dbg-state/debugger-config)) (seq (dbg-state/flow-exceptions flow-id))))) (flows-screen/goto-location bookmark-location)))) (defn data-window-push-val-data-event [{:keys [dw-id val-data extras]}] (data-windows/push-val dw-id val-data extras)) (defn data-window-update-event [{:keys [dw-id data]}] (data-windows/update-val dw-id data)) (defn process-event [[ev-type ev-args-map]] (case ev-type :vanilla-var-instrumented (vanilla-var-instrumented-event ev-args-map) :vanilla-var-uninstrumented (vanilla-var-uninstrumented-event ev-args-map) :vanilla-namespace-instrumented (vanilla-namespace-instrumented-event ev-args-map) :vanilla-namespace-uninstrumented (vanilla-namespace-uninstrumented-event ev-args-map) :storm-instrumentation-updated-event (storm-instrumentation-updated-event ev-args-map) :flow-created (flow-created-event ev-args-map) :flow-discarded (flow-discarded-event ev-args-map) :threads-updated (threads-updated-event ev-args-map) :timeline-updated (timeline-updated-event ev-args-map) :tap (tap-event ev-args-map) :out-write (out-write-event ev-args-map) :err-write (err-write-event ev-args-map) :last-evals-update (last-evals-update-event ev-args-map) :task-submitted (task-submitted-event ev-args-map) :task-finished (task-finished-event ev-args-map) :task-failed (task-failed-event ev-args-map) :heap-info-update (heap-info-update-event ev-args-map) :goto-location (goto-location-event ev-args-map) :show-doc (show-doc-event ev-args-map) :break-installed (break-installed-event ev-args-map) :break-removed (break-removed-event ev-args-map) :recording-updated (recording-updated-event ev-args-map) :multi-timeline-recording-updated (multi-timeline-recording-updated-event ev-args-map) :function-unwinded-event (function-unwinded-event ev-args-map) :expression-bookmark-event (expression-bookmark-event ev-args-map) :data-window-push-val-data (data-window-push-val-data-event ev-args-map) :data-window-update (data-window-update-event ev-args-map) nil ;; events-processor doesn't handle every event, specially tasks processing )) ================================================ FILE: src-dbg/flow_storm/debugger/events_queue.clj ================================================ (ns flow-storm.debugger.events-queue "Namespace for the sub-component that manages an events queue. This events are pushed by the runtime part (where the recordings live) via direct calling in the local mode or via websockets in remote mode for letting us (the debugger) know about interesting events on the runtime part of the world. Events will be dispatched after a dispatch-fn is set with `set-dispatch-fn`" (:require [flow-storm.state-management :refer [defstate]] [flow-storm.debugger.state :as dbg-state] [flow-storm.utils :as utils :refer [log]]) (:import [java.util.concurrent ArrayBlockingQueue TimeUnit])) (declare start-events-queue) (declare stop-events-queue) (declare events-queue) (defstate events-queue :start (fn [_] (start-events-queue)) :stop (fn [] (stop-events-queue))) (def queue-poll-interval 500) (def wait-for-system-started-interval 1000) (defn enqueue-event! [e] (when-let [queue (:queue events-queue)] (.put ^ArrayBlockingQueue queue e))) (defn add-dispatch-fn [fn-key dispatch-fn] (swap! (:dispatch-fns events-queue) assoc fn-key dispatch-fn)) (defn rm-dispatch-fn [fn-key] (swap! (:dispatch-fns events-queue) dissoc fn-key)) (defn start-events-queue [] (let [events-queue (ArrayBlockingQueue. 1000) dispatch-fns (atom {}) dispatch-thread (Thread. (fn [] (try ;; don't do anything until we have a dispatch-fn (while (and (not (.isInterrupted (Thread/currentThread))) (empty? @dispatch-fns)) (utils/log "Waiting for a dispatch-fn before dispatching events") (Thread/sleep wait-for-system-started-interval)) ;; start the dispatch loop (loop [ev nil] (when-not (.isInterrupted (Thread/currentThread)) (try (when ev (when (and (:debug-mode? (dbg-state/debugger-config)) (not (= (first ev) :heap-info-update))) (log (format "Processing event: %s" ev))) (doseq [dispatch (vals @dispatch-fns)] (dispatch ev))) (catch Exception e (utils/log-error (str "Error dispatching event" ev) e))) (recur (.poll events-queue queue-poll-interval TimeUnit/MILLISECONDS)))) (catch java.lang.InterruptedException _ (utils/log "Events thread interrupted")) (catch Exception e (utils/log-error "Events queue thread error" e)))) "FlowStorm Events Processor") interrupt-fn (fn [] (.interrupt dispatch-thread))] (.start dispatch-thread) {:interrupt-fn interrupt-fn :queue events-queue :dispatch-fns dispatch-fns})) (defn stop-events-queue [] (when-let [stop-fn (:interrupt-fn events-queue)] (stop-fn))) ================================================ FILE: src-dbg/flow_storm/debugger/main.clj ================================================ (ns flow-storm.debugger.main " This is the main namespace for the debugger itself, the graphical part of FlowStorm, being `start-debugger` the main entry point. The debugger system is made of varios stateful components orchestrated by a custom state system defined `flow-storm.state-management`, which is similar to `mount`. We are choosing a custom state system so we depend on as less libraries as we can to avoid version conflicts with users libraries. The debugger will start a different set of components depending if we are running a remote or local debugger. The debugger is a local debugger when it runs in the same process as the debuggee and can call all functions locally in contrast with remote debugging like when connecting to a ClojureScript system or a remote Clojure system where calls had to be made via websocket and repl connections. Look at each component namespace doc string for more details on them. " (:require [flow-storm.debugger.ui.main :as ui-main] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.events-queue :as events-queue] [flow-storm.debugger.events-processor :as events-processor] [flow-storm.debugger.docs] [flow-storm.debugger.runtime-api] [flow-storm.debugger.websocket] [flow-storm.debugger.repl.core :as repl-core] [flow-storm.utils :as utils] [flow-storm.state-management :as state-management] [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.plugins :as plugins] [clojure.string :as str])) (def flow-storm-core-ns 'flow-storm.core) (def local-debugger-state-vars "References to sub-components for local debugger sessions" [#'flow-storm.debugger.events-queue/events-queue #'flow-storm.debugger.docs/fn-docs #'flow-storm.debugger.state/state #'flow-storm.debugger.ui.main/ui #'flow-storm.debugger.runtime-api/rt-api]) (def remote-debugger-state-vars "Extends `local-debugger-state-vars` with extra sub-components for remote debugger sessions" (into local-debugger-state-vars [#'flow-storm.debugger.websocket/websocket-server #'flow-storm.debugger.repl.core/repl])) (defn stop-debugger "Gracefully stop the debugger. Useful for reloaded workflows." [] (state-management/stop {})) (defn debugger-config [] (let [theme-prop (System/getProperty "flowstorm.theme") title-prop (System/getProperty "flowstorm.title") styles-prop (System/getProperty "flowstorm.styles") auto-update-ui-prop (System/getProperty "flowstorm.autoUpdateUI") call-tree-update-prop (System/getProperty "flowstorm.callTreeUpdate") ui-timeout-millis (System/getProperty "flowstorm.uiTimeoutMillis") plugins-nss-set (->> (reduce-kv (fn [acc prop value] (if (str/starts-with? prop "flowstorm.plugins.namespaces") (into acc (str/split value #",")) acc)) #{} (System/getProperties)))] (cond-> {} theme-prop (assoc :theme (keyword theme-prop)) styles-prop (assoc :styles styles-prop) title-prop (assoc :title title-prop) auto-update-ui-prop (assoc :auto-update-ui? (= "true" auto-update-ui-prop)) call-tree-update-prop (assoc :call-tree-update? (= "true" call-tree-update-prop)) ui-timeout-millis (assoc :ui-timeout-millis (Long/parseLong ui-timeout-millis)) (seq plugins-nss-set) (assoc :plugins-namespaces-set plugins-nss-set)))) (defn start-debugger "Run the debugger. `config` should be a map containing : - `:local?` when false will also start a websocket server and listen for connections - `:theme` can be one of `:light`, `:dark` or `:auto` - `:styles` a string path to a css file if you want to override some started debugger styles When `:local?` is false you can also provide `:runtime-host` `:debugger-host` and `:port` for the nrepl server. `:runtime-host` should be the ip of the debuggee (defaults to localhost) `:debugger-host` shoud be the ip where the debugger is running, since the debuggee needs to connect back to it (defaults to localhost) `:pre-require` can be used with a symbol for requiring a ns before starting" [{:keys [local? pre-require] :as config}] (when pre-require (require pre-require)) ;; Ensure a task bar icon is shown on MacOS. (System/setProperty "apple.awt.UIElement" "false") ;; Initialize the JavaFX toolkit (ui-utils/init-toolkit) (let [config (-> config (update :port (fn [port] (or port (some-> (try (slurp ".nrepl-port") (catch Exception _)) Integer/parseInt)))) (merge (debugger-config)))] (plugins/load-plugins-namespaces config) (if local? ;; start components for local debugging (do (state-management/start {:only local-debugger-state-vars :config config}) (ui-main/setup-ui-from-runtime-config) (ui-main/setup-instrumentation-ui)) ;; else, start components for remote debugging (let [ws-connected? (promise) repl-connected? (promise) fully-started (atom false) signal-ws-connected (fn [conn?] (ui-main/set-conn-status-lbl :ws conn?) (dbg-state/set-connection-status :ws conn?)) signal-repl-connected (fn [conn?] (dbg-state/set-connection-status :repl conn?) (ui-main/set-conn-status-lbl :repl conn?))] (state-management/start {:only remote-debugger-state-vars :config (assoc config :on-ws-event events-queue/enqueue-event! :on-ws-down (fn [] (utils/log "WebSocket connection went away") (signal-ws-connected false) (ui-main/clear-ui)) :on-ws-up (fn [_] (signal-ws-connected true) (deliver ws-connected? true) ;; This is kind of hacky but will handle the ClojureScript page reload situation. ;; After a page reload all the runtime part has been restarted, so ;; we can re-init it through the repl and also re-setup the ui with whatever the ;; runtime contains in terms of settings. ;; But we need to skip this the first time the ws connection comes up ;; since maybe the system ins't fully started yet, we maybe don't even have a UI (when @fully-started (when (dbg-state/repl-config) (repl-core/init-repl (dbg-state/env-kind))) (ui-main/setup-ui-from-runtime-config))) :on-repl-down (fn [] (signal-repl-connected false)) :on-repl-up (fn [] (deliver repl-connected? true) (signal-repl-connected true)))}) (reset! fully-started true) ;; if there is a repl config wait for the connection before moving on (when (and (dbg-state/repl-config) @repl-connected?) (signal-repl-connected true)) ;; once we have both the UI started and the runtime-connected ;; initialize the UI with the info retrieved from the runtime (when @ws-connected? (signal-ws-connected true) (ui-main/setup-ui-from-runtime-config) (ui-main/setup-instrumentation-ui))))) ;; for both, local and remote ;; we set the events dispatch-fn afater `state-management/start` returns because ;; we know the UI is ready to start processing events (events-queue/add-dispatch-fn :events-processor events-processor/process-event) ) ================================================ FILE: src-dbg/flow_storm/debugger/repl/core.clj ================================================ (ns flow-storm.debugger.repl.core "Stateful component that handles debugger connection to repls. `start-repl` will be called at startup and will use the current repl config in `state` to start a repl connection. Depending on the repl configuration in `state` it can start Clojure and ClojureScript repl connections. It can do runtime initialization sequences with `init-repl` to prepare the runtime part by executing some repl instructions. The main functions after the repl is ready are : - `safe-eval-code-str` for evaluating Clojure - `safe-cljs-eval-code-str` for evaluating ClojureScript " (:require [flow-storm.state-management :refer [defstate]] [flow-storm.debugger.repl.nrepl :as nrepl] [flow-storm.debugger.websocket] [flow-storm.debugger.state :as dbg-state] [flow-storm.utils :as utils] [clojure.java.io :as io]) (:import [java.io OutputStream])) (declare start-repl) (declare stop-repl) (declare init-repl) (def repl-watchdog-interval 3000) (def log-file-path "./repl-client-debug") (declare repl) (defstate repl :start (fn [config] (start-repl config)) :stop (fn [] (stop-repl))) (defn default-repl-ns [] (let [env-kind (dbg-state/env-kind)] (case env-kind :clj "user" :cljs "shadow.user"))) (defn eval-code-str "Evaluate `code-str` in the connected Clojure repl. Will throw if anything goes wrong. `ns` can be used to customize what namespace `code-str`should be executed in. Returns the result object." ([code-str] (eval-code-str code-str nil)) ([code-str ns] (if-not (:repl-ready? (dbg-state/connection-status)) (utils/log-error "No repl available. You need a repl connection to use this functionality. Checkout the user guide.") (let [ns (or ns (default-repl-ns))] (when-let [repl-eval (:repl-eval repl)] (try (repl-eval code-str ns) (catch clojure.lang.ExceptionInfo ei (throw (ex-info "Error evaluating code on :clj repl" (assoc (ex-data ei) :code-str code-str :ns ns)))))))))) (defn safe-eval-code-str "Wrapper of `eval-code-str` that will not throw. Will just log an error in case of exception." [& args] (try (apply eval-code-str args) (catch Exception e (utils/log-error (.getMessage e) e)))) (defn safe-cljs-eval-code-str "Eval `code-str` in the clojurescript repl through the connected clojure repl. `ns` can be used to customize what namespace `code-str`should be executed in. In case anything goes wrong it will not throw, just log an error. Returns the result object. If it is a js object it will return a string." ([code-str] (safe-cljs-eval-code-str code-str nil)) ([code-str ns] (let [ns (or ns "cljs.user")] (if-let [repl-eval-cljs (:repl-eval-cljs repl)] (try (repl-eval-cljs code-str ns) (catch Exception e (utils/log-error (.getMessage e) e) (throw (ex-info "Error evaluating code on :cljs repl" {:code-str code-str :ns ns :cause e})))) (utils/log-error "No cljs repl available"))))) (defn make-cljs-repl-init-sequence [] [{:code "(do (in-ns 'shadow.user) nil)" :ns nil :repl-kind :clj} {:code "(require '[flow-storm.api :include-macros true])" :ns nil :repl-kind :clj} {:code "(require '[flow-storm.runtime.debuggers-api :include-macros true])" :ns "cljs.user" :repl-kind :cljs}]) (defn make-clj-repl-init-sequence [] (let [opts (select-keys (dbg-state/debugger-config) [:debugger-host :debugger-ws-port])] [{:code "(do (in-ns 'user) nil)" :ns nil :repl-kind :clj} {:code "(require '[flow-storm.api])" :ns "user" :repl-kind :clj} {:code (format "(flow-storm.runtime.debuggers-api/remote-connect %s)" (pr-str opts)) :ns "user" :repl-kind :clj}])) (defn init-repl ([env-kind] (init-repl env-kind (:repl-eval repl) (:repl-eval-cljs repl))) ([env-kind repl-eval repl-eval-cljs] (let [repl-init-sequence (case env-kind :clj (make-clj-repl-init-sequence) :cljs (make-cljs-repl-init-sequence))] (doseq [{:keys [code ns repl-kind]} repl-init-sequence] (case repl-kind :clj (repl-eval code ns) :cljs (repl-eval-cljs code ns)))))) (defn- connect-and-init [{:keys [repl-type runtime-host port build-id on-repl-up]}] (let [runtime-host (or runtime-host "localhost") env-kind (if (#{:shadow} repl-type) :cljs :clj) ;; HACKY, this logic is replicated in `state` repl-kind :nrepl ;; HACKY, this logic is replicated in `state` log-file (io/file log-file-path) ^OutputStream log-output-stream (io/make-output-stream log-file {:append true :encoding "UTF-8"}) connect (fn [] (case repl-kind :nrepl (nrepl/connect runtime-host port))) ;; repl here will be a map with :repl-eval (fn [code-str ns] ) and :close-connection (fn []) ;; :repl-eval fn will eval on the specific repl and return the value always as a string {:keys [repl-eval close-connection]} (connect) eval-clj (fn [code-str ns] (when-not (= code-str ":watch-dog-ping") (.write log-output-stream (.getBytes (format "\n\n---- [ %s ] ---->\n" ns))) (.write log-output-stream (.getBytes (pr-str code-str))) (.flush log-output-stream)) (let [response (repl-eval code-str ns)] (when-not (= code-str ":watch-dog-ping") (.write log-output-stream (.getBytes "\n<---------\n")) (.write log-output-stream (.getBytes (pr-str response))) (.flush log-output-stream)) (try (when response (read-string {} response)) (catch Exception e (utils/log-error (format "Error reading the response %s. CAUSE : %s" response (.getMessage e))))))) eval-cljs (fn [code-str ns] (eval-clj (format "((requiring-resolve 'hansel.instrument.utils/eval-in-ns-fn-cljs) '%s '%s %s)" ns code-str (pr-str {:build-id build-id})) "shadow.user")) repl-comp {:repl-eval eval-clj :repl-eval-cljs eval-cljs :close-connection (fn [] (close-connection) (.close log-output-stream))}] (utils/log "Initializing repl...") (try (init-repl env-kind eval-clj eval-cljs) (catch Exception e (utils/log-error "There was a problem initializing the remote runtime via repl" e))) (let [repl-ok? (= :watch-dog-ping (eval-clj ":watch-dog-ping" "user"))] (utils/log (str "Repl ok? : " repl-ok?)) (when repl-ok? (on-repl-up))) repl-comp)) (defn- watchdog-loop [{:keys [on-repl-down] :as config}] (let [repl-watchdog-thread (Thread. (fn [] (utils/log "Starting the repl watchdog loop") (try (loop [] (let [repl-ok? (try (= :watch-dog-ping (eval-code-str ":watch-dog-ping" "user")) (catch clojure.lang.ExceptionInfo ei (let [{:keys [error/type] :as exd} (ex-data ei)] (utils/log (format "[WATCHDOG] error executing ping. %s %s" type exd)) ;; just return that the repl is not ok when we got a socket-exception from ;; the underlying repl (not= type :repl/socket-exception))) (catch Exception e (utils/log-error "This is completely unexpected :") (.printStackTrace e) false))] (when-not repl-ok? (utils/log "[WATCHDOG] repl looks down, trying to reconnect ...") (on-repl-down) (stop-repl) (try (let [new-repl-cmp (connect-and-init config)] (alter-var-root #'repl (constantly new-repl-cmp))) (catch Exception e (utils/log (format "Couldn't restart repl (%s), retrying in %d ms" (.getMessage e) repl-watchdog-interval))))) (Thread/sleep repl-watchdog-interval)) (recur)) (catch java.lang.InterruptedException _ (utils/log "FlowStorm Repl Watchdog thread interrupted")) (catch Exception e (.printStackTrace e))))) repl-watchdog-interrupt (fn [] (.interrupt repl-watchdog-thread))] (.setName repl-watchdog-thread "FlowStorm Repl Watchdog") (.start repl-watchdog-thread) repl-watchdog-interrupt)) (defn start-repl [{:keys [port] :as config}] (when port (let [repl-comp (connect-and-init config)] (watchdog-loop config) repl-comp))) (defn stop-repl [] (when-let [close-conn (:close-connection repl)] (close-conn))) ================================================ FILE: src-dbg/flow_storm/debugger/repl/nrepl.clj ================================================ (ns flow-storm.debugger.repl.nrepl "Utilities to connect to nRepl servers" (:require [nrepl.core :as nrepl] [nrepl.transport :as transport] [flow-storm.utils :refer [log]])) (defn connect "Given `host` and `port` connects to a nRepl server. Returns a map with two functions {:repl-eval , :close-connection}. :repl-eval is a function of code and ns, both strings" [host port] (let [transport (nrepl/connect :host host :port port :transport-fn #'transport/bencode) ;; non of our commands should take longer than 10secs to execute ;; if more, we consider it a repl timeout and give up client (nrepl/client transport 10000) session (nrepl/client-session client)] {:repl-eval (fn repl-eval [code-str ns] (try (let [msg (cond-> {:op "eval" :code code-str} ns (assoc :ns ns)) responses (nrepl/message session msg) {:keys [err] :as res-map} (nrepl/combine-responses responses)] (if (empty? responses) (throw (ex-info "nrepl timeout" {:error/type :repl/timeout})) (if err (throw (ex-info (str "nrepl evaluation error: " err) (assoc res-map :error/type :repl/evaluation-error :msg msg))) (first (:value res-map))))) (catch java.net.SocketException se (throw (ex-info (.getMessage se) {:error/type :repl/socket-exception}))) (catch Exception e (log (format "Error evaluating %s in NS %s, CAUSE: %s" code-str ns (.getMessage e)))))) :close-connection (fn [] (.close transport))})) (comment (def transport (nrepl/connect :host "localhost" :port 9000 :transport-fn #'transport/bencode)) (def client (nrepl/client transport Long/MAX_VALUE)) (def client (nrepl/client transport 3000)) (def session (nrepl/client-session client)) (def res (nrepl/message session {:op "eval" :code "(require '[some.crazy :as c])"})) (def res (nrepl/message session {:op "eval" :code "(do (require '[shadow.cljs.devtools.api :as shadow]) (shadow/nrepl-select :browser-repl))"})) (def res (nrepl/message session {:op "describe"})) (def res (nrepl/message session {:op "ls-sessions"})) (def res (nrepl/message session {:op "eval" :code "(+ 1 2)"})) (def res (nrepl/message session {:op "eval" :code "(in-ns 'user)"})) (def res (nrepl/message session {:op "eval" :code "(do (require '[shadow.cljs.devtools.api :as shadow]) (shadow/watch :app) (shadow/nrepl-select :app))"})) (def res (nrepl/message session {:op "eval" :code "js/window"})) (def res (nrepl/message session {:op "eval" :code "a" :ns "cljs.user"})) ) ================================================ FILE: src-dbg/flow_storm/debugger/runtime_api.clj ================================================ (ns flow-storm.debugger.runtime-api "Component that implements the api that the debugger uses to call the runtime. All debugger functionality is implemented agains this API. The api is declared as a protocol `RuntimeApiP` and has two possible instantiations : - `LocalRuntimeApi` directly call functions, since we are on the same process - `RemoteRuntimeApi` call funcitons through a websocket and a repl All this is implemented runtime part in `flow-storm.runtime.debuggers-api` which is the interface exposed by the runtime to debuggers." (:require [flow-storm.state-management :refer [defstate]] [flow-storm.utils :as utils :refer [log log-error]] [flow-storm.debugger.repl.core :refer [safe-eval-code-str safe-cljs-eval-code-str stop-repl]] [flow-storm.runtime.debuggers-api :as dbg-api] [flow-storm.debugger.websocket :as websocket] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.ui.flows.general :refer [show-message]] [clojure.string :as str]) (:import [java.io Closeable])) (declare ->LocalRuntimeApi) (declare ->RemoteRuntimeApi) (declare rt-api) (declare api-call) (def ^:dynamic *cache-disabled?* false) (defstate rt-api :start (fn [{:keys [local?]}] (let [api-cache (atom {})] (if local? (->LocalRuntimeApi api-cache) (->RemoteRuntimeApi api-cache)))) :stop (fn [] (when-let [cc (:close-connection rt-api)] (cc)))) (defprotocol RuntimeApiP (runtime-config [_]) (val-pprint [_ v opts]) (data-window-push-val-data [_ dw-id val-ref extra]) (get-form [_ form-id]) (timeline-count [_ flow-id thread-id]) (timeline-entry [_ flow-id thread-id idx drift]) (multi-thread-timeline-count [_ flow-id]) (frame-data [_ flow-id thread-id idx opts]) (bindings [_ flow-id thread-id idx opts]) (callstack-tree-root-node [_ flow-id thread-id]) (callstack-node-childs [_ node]) (callstack-node-frame [_ node]) (fn-call-stats [_ flow-id thread-id]) (interrupt-all-tasks [_]) (start-task [_ task-id]) (collect-fn-frames-task [_ flow-id thread-id fn-ns fn-name form-id render-args render-ret?]) (find-expr-entry-task [_ criteria]) (total-order-timeline-task [_ opts]) (thread-prints-task [_ print-cfg]) (search-collect-timelines-entries-task [_ criteria opts]) (discard-flow [_ flow-id]) (def-value [_ var-symb val-ref]) (tap-value [_ v]) (get-all-namespaces [_]) (get-all-vars-for-ns [_ nsname]) (get-var-meta [_ var-ns var-name]) (vanilla-instrument-var [_ var-ns var-name opts]) (vanilla-uninstrument-var [_ var-ns var-name opts]) (vanilla-instrument-namespaces [_ nsnames opts]) (vanilla-uninstrument-namespaces [_ nanames opts]) (modify-storm-instrumentation [_ operation opts]) (get-storm-instrumentation [_]) (storm-instrumentation-enable? [_]) (turn-storm-instrumentation [_ on?]) (reload-namespace [_ ns-info]) (eval-form [_ form-str opts]) (clear-runtime-state [_]) (clear-api-cache [_]) (clear-outputs [_]) (all-flows-threads [_]) (flow-threads-info [_ flow-id]) (unblock-thread [_ thread-id]) (unblock-all-threads [_]) (add-breakpoint [_ fq-fn-symb opts]) (remove-breakpoint [_ fq-fn-symb opts]) (stack-for-frame [_ flow-id thread-id fn-call-idx]) (toggle-recording [_]) (toggle-multi-timeline-recording [_]) (switch-record-to-flow [_ flow-id]) (find-fn-call-task [_ fq-fn-call-symb from-idx opts]) (set-thread-trace-limit [_ limit]) (set-heap-limit [_ limit]) (call-by-fn-key [_ fn-key args-vec])) (defn cached-apply [cache cache-key f args] (let [res (get @cache cache-key :flow-storm/cache-miss)] (if (or *cache-disabled?* (= res :flow-storm/cache-miss)) ;; miss or disabled, we need to call (let [new-res (apply f args)] (when (:debug-mode? (dbg-state/debugger-config)) (log (utils/colored-string "CALLED" :red))) (swap! cache assoc cache-key new-res) new-res) ;; hit, return cached (do (when (:debug-mode? (dbg-state/debugger-config)) (log "CACHED")) res)))) (defn api-call "Calling the runtime side function fkey with args. The call-type can be :local or :remote. If a callback is provided then this function will be async, nil will be returned and the callback will be called with the response. Otherwise it will block and return the response." ([call-type fkey args] (api-call call-type fkey args {} nil)) ([call-type fkey args opts] (api-call call-type fkey args opts nil)) ([call-type fkey args {:keys [cache]} callback] (let [timeout (:ui-timeout-millis (dbg-state/debugger-config)) f (case call-type :local (dbg-api/api-fn-by-key fkey) :remote (fn [& args] (websocket/sync-remote-api-request fkey args))) debug-mode? (:debug-mode? (dbg-state/debugger-config))] (when debug-mode? (log (format "%s API-CALL %s %s" call-type fkey (pr-str args)))) ;; make the calls in a future so we can have a timeout and don't block the UI thread ;; forever (let [call-resp-fut (future (let [result (if cache (let [cache-key (into [fkey] args)] (cached-apply cache cache-key f args)) (do (when debug-mode? (log (utils/colored-string "CALLED" :red))) (apply f args)))] (if callback (callback result) result)))] (when-not callback (let [call-resp (deref call-resp-fut timeout :flow-storm/call-time-out)] (if (= call-resp :flow-storm/call-time-out) (let [msg (format "A call to %s timed out" fkey)] (show-message msg :warning) (throw (ex-info msg {:call-type call-type :fkey fkey :args args}))) call-resp))))))) (defn- config-dw-extras [extras] (assoc extras :pprint-previews? (:pprint-previews? (dbg-state/debugger-config)))) (defrecord LocalRuntimeApi [api-cache] RuntimeApiP (runtime-config [_] (api-call :local :runtime-config [])) (val-pprint [_ v opts] (api-call :local :val-pprint [v opts] {:cache api-cache})) ;; CACHED (data-window-push-val-data [_ dw-id val-ref extra] (api-call :local :data-window-push-val-data [dw-id val-ref (config-dw-extras extra)])) (get-form [_ form-id] (api-call :local :get-form [form-id] {:cache api-cache})) ;; CACHED (timeline-count [_ flow-id thread-id] (api-call :local :timeline-count [flow-id thread-id])) (timeline-entry [_ flow-id thread-id idx drift] (api-call :local :timeline-entry [flow-id thread-id idx drift])) (multi-thread-timeline-count [_ flow-id] (api-call :local :multi-thread-timeline-count [flow-id])) (frame-data [_ flow-id thread-id idx opts] (api-call :local :frame-data [flow-id thread-id idx opts])) (bindings [_ flow-id thread-id idx opts] (api-call :local :bindings [flow-id thread-id idx opts])) (callstack-tree-root-node [_ flow-id thread-id] (api-call :local :callstack-tree-root-node [flow-id thread-id])) (callstack-node-childs [_ node] (api-call :local :callstack-node-childs [node])) (callstack-node-frame [_ node] (api-call :local :callstack-node-frame [node])) (fn-call-stats [_ flow-id thread-id] (api-call :local :fn-call-stats [flow-id thread-id])) (collect-fn-frames-task [_ flow-id thread-id fn-ns fn-name form-id render-args render-ret?] (api-call :local :collect-fn-frames-task [flow-id thread-id fn-ns fn-name form-id render-args render-ret?])) (start-task [_ task-id] (api-call :local :start-task [task-id])) (interrupt-all-tasks [_] (api-call :local :interrupt-all-tasks [])) (find-expr-entry-task [_ criteria] (api-call :local :find-expr-entry-task [criteria])) (total-order-timeline-task [_ opts] (api-call :local :total-order-timeline-task [opts])) (thread-prints-task [_ print-cfg] (api-call :local :thread-prints-task [print-cfg])) (search-collect-timelines-entries-task [_ criteria opts] (api-call :local :search-collect-timelines-entries-task [criteria opts])) (discard-flow [_ flow-id] (api-call :local :discard-flow [flow-id])) (def-value [_ var-symb val-ref] (api-call :local :def-value [(or (namespace var-symb) "user") (name var-symb) val-ref])) (tap-value [_ vref] (api-call :local :tap-value [vref])) (get-all-namespaces [_] (mapv (comp str ns-name) (all-ns))) (get-all-vars-for-ns [_ nsname] (->> (ns-interns (symbol nsname)) keys (map str))) (get-var-meta [_ var-ns var-name] (-> (meta (resolve (symbol var-ns var-name))) (update :ns (comp str ns-name)))) (vanilla-instrument-var [_ var-ns var-name opts] (api-call :local :vanilla-instrument-var [:clj (symbol var-ns var-name) opts])) (vanilla-uninstrument-var [_ var-ns var-name opts] (api-call :local :vanilla-uninstrument-var [:clj (symbol var-ns var-name) opts])) (vanilla-instrument-namespaces [_ nsnames {:keys [profile] :as opts}] (let [disable-set (utils/disable-from-profile profile)] (api-call :local :vanilla-instrument-namespaces [:clj nsnames (assoc opts :disable disable-set)]))) (vanilla-uninstrument-namespaces [_ nsnames opts] (api-call :local :vanilla-uninstrument-namespaces [:clj nsnames opts])) (modify-storm-instrumentation [_ operation opts] (api-call :local :modify-storm-instrumentation [:clj operation opts])) (get-storm-instrumentation [_] (api-call :local :get-storm-instrumentation [:clj])) (storm-instrumentation-enable? [_] (api-call :local :storm-instrumentation-enable? [:clj])) (turn-storm-instrumentation [_ on?] (api-call :local :turn-storm-instrumentation [:clj on?])) (reload-namespace [_ ns-info] (require (symbol (:namespace-name ns-info)) :reload)) (eval-form [_ form-str {:keys [instrument? instrument-options var-name ns]}] (let [ns-to-eval (find-ns (symbol ns))] (binding [*ns* ns-to-eval] (let [form (read-string (if instrument? (format "(flow-storm.api/instrument* %s %s)" (pr-str instrument-options) form-str) form-str)) [v vmeta] (when var-name (let [v (find-var (symbol ns var-name))] [v (meta v)]))] ;; Don't eval the form in the same UI thread or ;; it will deadlock because the same thread can generate ;; a event that will be waiting for the UI thread (.start (Thread. (fn [] (binding [*ns* ns-to-eval] (try (eval form) (catch Exception ex (log-error "Error instrumenting form" ex) (if (and (.getCause ex) (str/includes? (.getMessage (.getCause ex)) "Method code too large!")) (show-message "The form you are trying to instrument exceeds the method limit after instrumentation. Try intrumenting it without bindings." :error) (show-message (.getMessage ex) :error)))) ;; when we evaluate a function from the repl we lose all meta ;; so when re-evaluating a var (probably a function) store and restore its meta (when v (reset-meta! v vmeta)))))))))) (clear-runtime-state [_] (api-call :local :clear-runtime-state [])) (clear-api-cache [_] (reset! api-cache {})) (clear-outputs [_] (api-call :local :clear-outputs [])) (flow-threads-info [_ flow-id] (api-call :local :flow-threads-info [flow-id])) (unblock-thread [_ thread-id] (api-call :local :unblock-thread [thread-id])) (unblock-all-threads [_] (api-call :local :unblock-all-threads [])) (add-breakpoint [_ fq-fn-symb opts] (api-call :local :add-breakpoint! [fq-fn-symb opts])) (remove-breakpoint [_ fq-fn-symb opts] (api-call :local :remove-breakpoint! [fq-fn-symb opts])) (all-flows-threads [_] (api-call :local :all-flows-threads [])) (stack-for-frame [_ flow-id thread-id fn-call-idx] (api-call :local :stack-for-frame [flow-id thread-id fn-call-idx])) (toggle-recording [_] (api-call :local :toggle-recording [])) (toggle-multi-timeline-recording [_] (api-call :local :toggle-multi-timeline-recording [])) (switch-record-to-flow [_ flow-id] (api-call :local :switch-record-to-flow [flow-id])) (find-fn-call-task [_ fq-fn-call-symb from-idx opts] (api-call :local :find-fn-call-task [fq-fn-call-symb from-idx opts])) (set-thread-trace-limit [_ limit] (api-call :local :set-thread-trace-limit [limit])) (set-heap-limit [_ limit] (api-call :local :set-heap-limit [limit])) (call-by-fn-key [_ fn-key args-vec] (api-call :local fn-key args-vec))) ;;;;;;;;;;;;;;;;;;;;;; ;; For Clojure repl ;; ;;;;;;;;;;;;;;;;;;;;;; (defrecord RemoteRuntimeApi [api-cache] RuntimeApiP (runtime-config [_] (api-call :remote :runtime-config [])) (val-pprint [_ v opts] (api-call :remote :val-pprint [v opts] {:cache api-cache})) ;; CACHED (data-window-push-val-data [_ dw-id val-ref extra] (api-call :remote :data-window-push-val-data [dw-id val-ref (config-dw-extras extra)])) (get-form [_ form-id] (api-call :remote :get-form [form-id] {:cache api-cache})) ;; CACHED (timeline-count [_ flow-id thread-id] (api-call :remote :timeline-count [flow-id thread-id])) (timeline-entry [_ flow-id thread-id idx drift] (api-call :remote :timeline-entry [flow-id thread-id idx drift])) (multi-thread-timeline-count [_ flow-id] (api-call :remote :multi-thread-timeline-count [flow-id])) (frame-data [_ flow-id thread-id idx opts] (api-call :remote :frame-data [flow-id thread-id idx opts])) (bindings [_ flow-id thread-id idx opts] (api-call :remote :bindings [flow-id thread-id idx opts])) (callstack-tree-root-node [_ flow-id thread-id] (api-call :remote :callstack-tree-root-node [flow-id thread-id])) (callstack-node-childs [_ node] (api-call :remote :callstack-node-childs [node])) (callstack-node-frame [_ node] (api-call :remote :callstack-node-frame [node])) (fn-call-stats [_ flow-id thread-id] (api-call :remote :fn-call-stats [flow-id thread-id])) (collect-fn-frames-task [_ flow-id thread-id fn-ns fn-name form-id render-args render-ret?] (api-call :remote :collect-fn-frames-task [flow-id thread-id fn-ns fn-name form-id render-args render-ret?])) (start-task [_ task-id] (api-call :remote :start-task [task-id])) (interrupt-all-tasks [_] (api-call :remote :interrupt-all-tasks [])) (find-expr-entry-task [_ criteria] (api-call :remote :find-expr-entry-task [criteria])) (total-order-timeline-task [_ opts] (api-call :remote :total-order-timeline-task [opts])) (thread-prints-task [_ print-cfg] (api-call :remote :thread-prints-task [print-cfg])) (search-collect-timelines-entries-task [_ criteria opts] (api-call :remote :search-collect-timelines-entries-task [criteria opts])) (discard-flow [_ flow-id] (api-call :remote :discard-flow [flow-id])) (def-value [_ var-symb val-ref] (case (dbg-state/env-kind) :clj (api-call :remote :def-value [(or (namespace var-symb) "user") (name var-symb) val-ref]) :cljs (safe-cljs-eval-code-str (format "(def %s (flow-storm.runtime.values/deref-value (flow-storm.types/make-value-ref %d)))" (name var-symb) (:vid val-ref)) (or (namespace var-symb) "cljs.user")))) (tap-value [_ vref] (api-call :remote :tap-value [vref])) (get-all-namespaces [_] (case (dbg-state/env-kind) :clj (api-call :remote :all-namespaces [:clj]) :cljs (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/all-namespaces :cljs %s)" (:repl.cljs/build-id (dbg-state/repl-config)))))) (get-all-vars-for-ns [_ nsname] (case (dbg-state/env-kind) :clj (api-call :remote :all-vars-for-namespace [:clj (symbol nsname)]) :cljs (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/all-vars-for-namespace :cljs '%s %s)" nsname (:repl.cljs/build-id (dbg-state/repl-config)))))) (get-var-meta [_ var-ns var-name] (case (dbg-state/env-kind) :clj (api-call :remote :get-var-meta [:clj (symbol var-ns var-name)]) :cljs (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/get-var-meta :cljs '%s/%s %s)" var-ns var-name {:build-id (:repl.cljs/build-id (dbg-state/repl-config))})))) (vanilla-instrument-var [_ var-ns var-name opts] (case (dbg-state/env-kind) :clj (api-call :remote :vanilla-instrument-var [:clj (symbol var-ns var-name) opts]) :cljs (let [opts (assoc opts :build-id (:repl.cljs/build-id (dbg-state/repl-config)))] (show-message "FlowStorm ClojureScript single var instrumentation is pretty limited. You can instrument them only once, and the only way of uninstrumenting them is by reloading your page or restarting your node process. Also deep instrumentation is missing some cases. So for most cases you are going to be better with [un]instrumenting entire namespaces." :warning) (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/vanilla-instrument-var :cljs '%s/%s %s)" var-ns var-name opts))))) (vanilla-uninstrument-var [_ var-ns var-name opts] (case (dbg-state/env-kind) :clj (api-call :remote :vanilla-uninstrument-var [:clj (symbol var-ns var-name) opts]) :cljs (let [_opts (assoc opts :build-id (:repl.cljs/build-id (dbg-state/repl-config)))] (show-message "FlowStorm currently can't uninstrument single vars in ClojureScript. You can only [un]instrument entire namespaces. If you want to get rid of the current vars instrumentation please reload your browser page, or restart your node process." :warning) #_(safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/uninstrument-var :cljs '%s/%s %s)" var-ns var-name opts))))) (vanilla-instrument-namespaces [_ nsnames {:keys [profile] :as opts}] (let [opts (assoc opts :disable (utils/disable-from-profile profile))] (case (dbg-state/env-kind) :cljs (let [opts (assoc opts :build-id (:repl.cljs/build-id (dbg-state/repl-config)))] (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/vanilla-instrument-namespaces :cljs %s %s)" (into #{} nsnames) opts))) :clj (api-call :remote :vanilla-instrument-namespaces [:clj (into #{} nsnames) opts])))) (vanilla-uninstrument-namespaces [_ nsnames opts] (case (dbg-state/env-kind) :cljs (let [opts (assoc opts :build-id (:repl.cljs/build-id (dbg-state/repl-config)))] (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/vanilla-uninstrument-namespaces :cljs %s %s)" (into #{} nsnames) opts))) ;; for Clojure just call the api :clj (api-call :remote :vanilla-uninstrument-namespaces [:clj (into #{} nsnames) opts]))) (modify-storm-instrumentation [_ operation opts] (case (dbg-state/env-kind) :cljs (let [opts (assoc opts :build-id (:repl.cljs/build-id (dbg-state/repl-config)))] (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/modify-storm-instrumentation :cljs %s %s)" operation opts))) ;; for Clojure just call the api :clj (api-call :remote :modify-storm-instrumentation [:clj operation opts]))) (get-storm-instrumentation [_] (case (dbg-state/env-kind) :cljs (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/get-storm-instrumentation :cljs)")) ;; for Clojure just call the api :clj (api-call :remote :get-storm-instrumentation [:clj]))) (storm-instrumentation-enable? [_] (case (dbg-state/env-kind) :cljs (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/storm-instrumentation-enable? :cljs)")) ;; for Clojure just call the api :clj (api-call :remote :storm-instrumentation-enable? [:clj]))) (turn-storm-instrumentation [_ on?] (case (dbg-state/env-kind) :cljs (safe-eval-code-str (format "(flow-storm.runtime.debuggers-api/turn-storm-instrumentation :cljs %s)" on?)) ;; for Clojure just call the api :clj (api-call :remote :turn-storm-instrumentation [:clj on?]))) (reload-namespace [_ ns-info] (let [reload-code (format "(require '%s :reload)" (:namespace-name ns-info))] (case (dbg-state/env-kind) :cljs (safe-cljs-eval-code-str reload-code "cljs.user") :clj (safe-eval-code-str reload-code)))) (eval-form [this form-str {:keys [instrument? instrument-options var-name ns]}] (let [var-meta (when var-name (select-keys (get-var-meta this ns var-name) [:file :column :end-column :line :end-line])) form-expr (if instrument? (format "(flow-storm.api/instrument* %s %s)" (pr-str instrument-options) form-str) form-str) expr-res (case (dbg-state/env-kind) :clj (safe-eval-code-str form-expr ns) :cljs (safe-cljs-eval-code-str form-expr ns) )] (when (and (= :clj (dbg-state/env-kind)) var-meta) ;; for vars restore the meta attributes that get lost when we re eval from the repl (safe-eval-code-str (format "(alter-meta! #'%s/%s merge %s)" ns var-name (pr-str var-meta)))) expr-res)) (clear-runtime-state [_] (api-call :remote :clear-runtime-state [])) (clear-api-cache [_] (reset! api-cache {})) (clear-outputs [_] (api-call :remote :clear-outputs [])) (flow-threads-info [_ flow-id] (api-call :remote :flow-threads-info [flow-id])) (unblock-thread [_ thread-id] (api-call :remote :unblock-thread [thread-id])) (unblock-all-threads [_] (api-call :remote :unblock-all-threads [])) (add-breakpoint [_ fq-fn-symb opts] (case (dbg-state/env-kind) :clj (api-call :remote :add-breakpoint! [fq-fn-symb opts]) :cljs (show-message "Operation not supported for ClojureScript" :warning))) (remove-breakpoint [_ fq-fn-symb opts] (api-call :remote :remove-breakpoint! [fq-fn-symb opts])) (all-flows-threads [_] (api-call :remote :all-flows-threads [])) (stack-for-frame [_ flow-id thread-id fn-call-idx] (api-call :remote :stack-for-frame [flow-id thread-id fn-call-idx])) (toggle-recording [_] (api-call :remote :toggle-recording [])) (toggle-multi-timeline-recording [_] (api-call :remote :toggle-multi-timeline-recording [])) (switch-record-to-flow [_ flow-id] (api-call :remote :switch-record-to-flow [flow-id])) (find-fn-call-task [_ fq-fn-call-symb from-idx opts] (api-call :remote :find-fn-call-task [fq-fn-call-symb from-idx opts])) (set-thread-trace-limit [_ limit] (api-call :remote :set-thread-trace-limit [limit])) (set-heap-limit [_ limit] (api-call :remote :set-heap-limit [limit])) (call-by-fn-key [_ fn-key args-vec] (api-call :remote fn-key args-vec)) Closeable (close [_] (stop-repl)) ) ================================================ FILE: src-dbg/flow_storm/debugger/state.clj ================================================ (ns flow-storm.debugger.state "Sub component that manages the state of the debugger. This is the state for supporting the UI, not the runtime part, where the timelines are recorded. All the state is inside one atom `state` which is specified by the `::state` spec." (:require [flow-storm.state-management :refer [defstate]] [flow-storm.utils :refer [pop-n] :as utils] [flow-storm.debugger.ui.plugins :as plugins] [clojure.java.io :as io] [clojure.spec.alpha :as s]) (:import [javafx.stage Stage])) (s/def ::timestamp (s/nilable int?)) (s/def ::print-level int?) (s/def ::print-length int?) (s/def :flow-storm/timeline-entry map?) (s/def :flow-storm/val-id int?) (s/def :flow-storm/val-ref record? #_(s/keys :req-un [:flow-storm/val-id])) (s/def :flow-storm/fn-name string?) (s/def :flow-storm/fn-ns string?) (s/def :flow-storm/form-id int?) (s/def :flow-storm/coord (s/coll-of int?)) (s/def :flow-storm.frame/ret :flow-storm/val-ref) (s/def :flow-storm.frame/fn-call-idx int?) (s/def :flow-storm.frame/parent-fn-call-idx (s/nilable int?)) (s/def :flow-storm.frame/args-vec any? #_(s/coll-of :flow-storm/val-ref)) ;; TODO: fix this https://clojure.atlassian.net/browse/CLJ-1975 (s/def :flow-storm.frame/expr-executions (s/coll-of :flow-storm/timeline-entry)) ;; TODO: this could be refined, since they can only by :expr and :fn-return (s/def :return/kind #{:waiting :unwind :return}) (s/def :flow-storm/frame (s/keys :req [:return/kind] :req-un [:flow-storm.frame/fn-call-idx :flow-storm.frame/parent-fn-call-idx :flow-storm/fn-name :flow-storm/fn-ns :flow-storm/form-id :flow-storm.frame/args-vec :flow-storm.frame/expr-executions] :opt-un [:flow-storm.frame/ret :flow-storm.frame/throwable])) (s/def :thread/id int?) (s/def :thread/name string?) (s/def :thread/blocked (s/nilable (s/tuple :flow-storm/fn-ns :flow-storm/fn-name))) (s/def :thread/curr-timeline-entry (s/nilable :flow-storm/timeline-entry)) (s/def :thread/curr-frame :flow-storm/frame) (s/def :thread.ui.callstack-tree-hidden-fns/ref (s/keys :req-un [:flow-storm/fn-name :flow-storm/fn-ns])) (s/def :thread.ui/selected-functions-list-fn (s/nilable (s/keys :req-un [:flow-storm/fn-name :flow-storm/fn-ns :flow-storm/form-id]))) (s/def :thread.ui/callstack-tree-hidden-fns (s/coll-of :thread.ui.callstack-tree-hidden-fns/ref)) (s/def :navigation-history/history (s/coll-of :flow-storm/timeline-entry)) (s/def :thread/navigation-history (s/keys :req-un [:navigation-history/head-pos :navigation-history/history])) (s/def :thread.exception/ex-type string?) (s/def :thread.exception/ex-message string?) (s/def :thread.exception/ex-hash int?) (s/def :thread/exception (s/keys :req-un [:flow-storm/fn-name :flow-storm/fn-ns :thread.exception/ex-type :thread.exception/ex-message :thread.exception/ex-hash])) (s/def :flow/exceptions (s/map-of :thread.exception/ex-hash :thread/exception)) (s/def :flow/exceptions-limit-reached? boolean?) (s/def :flow/thread (s/keys :req [:thread/id :thread/curr-timeline-entry :thread/navigation-history] :opt [:thread/curr-frame :thread.ui/callstack-tree-hidden-fns :thread.ui/selected-functions-list-fn])) (s/def :flow/threads (s/map-of :thread/id :flow/thread)) (s/def :flow/id int?) (s/def :flow/flow (s/keys :req [:flow/id :flow/threads :flow/exceptions :flow/exceptions-limit-reached?] :req-un [::timestamp])) (s/def :flow/flows (s/map-of :flow/id :flow/flow)) (s/def :thread/info (s/keys :req [:flow/id :thread/id :thread/name] :opt [:thread/blocked])) (s/def :flow/threads-info (s/map-of :flow/id :thread/info)) (s/def :printer/enable? boolean?) (s/def :printer/source-expr any?) (s/def :printer/transform-expr-str string?) (s/def :printer/printer (s/keys :req-un [:flow-storm/coord ::print-level ::print-length :printer/enable? :printer/transform-expr-str :printer/source-expr])) (s/def :printer/flow-printers (s/map-of :flow-storm/form-id (s/map-of :flow-storm/coord :printer/printer))) (s/def :printer/printers (s/map-of :flow/id :printer/flow-printers)) (s/def :ui/selected-font-size-style-idx int?) (s/def :ui/selected-theme #{:light :dark}) (s/def :ui/selected-tool keyword?) (s/def :ui/extra-styles (s/nilable string?)) (s/def ::ws-ready? boolean?) (s/def ::repl-ready? boolean?) (s/def ::connection-status (s/keys :req-un [::ws-ready? ::repl-ready?])) (s/def :ui.object/id string?) (s/def :ui.object/node any?) (s/def :ui.jfx-nodes-index/flow-id (s/nilable (s/or :flow-id :flow/id :no-flow #{:no-flow}))) (s/def :ui/jfx-nodes-index (s/map-of (s/tuple :ui.jfx-nodes-index/flow-id (s/nilable :thread/id) :ui.object/id) (s/coll-of :ui.object/node))) (s/def ::jfx-stage #(instance? Stage %)) (s/def :ui/jfx-stages (s/coll-of ::jfx-stage)) (s/def :task/event-key keyword?) (s/def :task/id any?) (s/def ::pending-tasks-subscriptions (s/map-of (s/tuple :task/event-key :task/id) fn?)) (s/def ::clojure-storm-env? boolean?) (s/def ::local-mode? boolean?) (s/def :config/env-kind #{:clj :cljs}) (s/def :config/storm? boolean?) (s/def :config/flow-storm-nrepl-middleware? boolean?) (s/def :status/recording? boolean?) (s/def :status/total-order-recording? boolean?) (s/def :status/breakpoints (s/coll-of (s/tuple :flow-storm/fn-ns :flow-storm/fn-name))) (s/def ::runtime-config (s/nilable (s/keys :req-un [:config/env-kind :config/storm? :config/flow-storm-nrepl-middleware? :status/recording? :status/total-order-recording? :status/breakpoints]))) (s/def :repl/kind #{:nrepl}) (s/def :repl/type #{:shadow :clojure}) (s/def :repl/port int?) (s/def :repl.cljs/build-id keyword?) (s/def :config/repl (s/nilable (s/keys :req [:repl/kind :repl/type :repl/port] :opt [:repl.cljs/build-id]))) (s/def :config/debugger-host string?) (s/def :config/debugger-ws-port int?) (s/def :config/runtime-host string?) (s/def :config/debug-mode? boolean?) (s/def :config/auto-jump-on-exception? boolean?) (s/def :config/auto-update-ui? boolean?) (s/def :config/ui-timeout-millis number?) (s/def :config/pprint-previews? boolean?) (s/def ::debugger-config (s/keys :req-un [:config/repl :config/debugger-host :config/debugger-ws-port :config/runtime-host :config/debug-mode? :config/pprint-previews? :config/auto-jump-on-exception? :config/auto-update-ui? :config/ui-timeout-millis :config/call-tree-update?])) (s/def :bookmark/id (s/tuple :flow/id :thread/id int?)) (s/def :bookmark/flow-id :flow/id) (s/def :bookmark/thread-id :thread/id) (s/def :bookmark/idx int?) (s/def :bookmark/note (s/nilable string?)) (s/def :bookmark/source #{:bookmark.source/ui :bookmark.source/api}) (s/def ::bookmark (s/keys :req-un [:bookmark/flow-id :bookmark/thread-id :bookmark/idx :bookmark/note :bookmark/source])) (s/def ::bookmarks (s/map-of :bookmark/id ::bookmark)) (s/def :data-window/breadcrums-box :ui.object/node) (s/def :data-window/visualizers-combo-box :ui.object/node) (s/def :data-window/val-box :ui.object/node) (s/def :data-window/type-lbl :ui.object/node) (s/def :visualizer/on-create ifn?) (s/def :visualizer/on-update ifn?) (s/def :visualizer/on-destroy ifn?) (s/def :data-window.frame/val-data map?) (s/def :data-window.frame/visualizer-combo :ui.object/node) (s/def :data-window.frame/visualizer (s/keys :req-un [:visualizer/on-create] :opt-un [:visualizer/on-update :visualizer/on-destroy])) (s/def :fx/node :ui.object/node) (s/def :data-window.frame/visualizer-val-ctx (s/keys :req [:fx/node])) (s/def :data-window/frame (s/keys :req-un [:data-window.frame/val-data :data-window.frame/visualizer-combo :data-window.frame/visualizer :data-window.frame/visualizer-val-ctx])) (s/def :data-window/stack (s/coll-of :data-window/frame)) (s/def :data-windows/data-window (s/keys :req-un [:data-window/breadcrums-box :data-window/visualizers-combo-box :data-window/val-box :data-window/type-lbl :data-window/stack])) (s/def :data-window/id any?) (s/def ::data-windows (s/map-of :data-window/id :data-windows/data-window)) (s/def ::state (s/keys :req-un [:flow/flows :flow/threads-info :printer/printers :ui/selected-font-size-style-idx :ui/selected-theme :ui/selected-tool :ui/extra-styles :ui/jfx-nodes-index :ui/jfx-stages ::pending-tasks-subscriptions ::connection-status ::local-mode? ::runtime-config ::debugger-config ::bookmarks ::data-windows])) (defn initial-state [{:keys [theme styles local? port repl-type debugger-host ws-port runtime-host auto-update-ui? ui-timeout-millis call-tree-update?] :as config}] {:flows {} :printers {} :selected-font-size-style-idx 0 :threads-info {} :selected-theme (case theme :light :light :dark :dark :auto ((requiring-resolve 'flow-storm.debugger.ui.utils/get-current-os-theme)) :light) :selected-tool :tool-flows :local-mode? (boolean local?) :extra-styles styles :jfx-nodes-index {} :jfx-stages [] :pending-tasks-subscriptions {} :runtime-config nil :connection-status {:ws-ready? false :repl-ready? false} :debugger-config {:repl (when port (cond-> {:repl/kind :nrepl :repl/type (or repl-type :clojure) :repl/port port} (#{:shadow} repl-type) (assoc :repl.cljs/build-id (:build-id config)))) :debugger-host (or debugger-host "localhost") :debugger-ws-port (or ws-port 7722) :runtime-host (or runtime-host "localhost") :debug-mode? false :auto-jump-on-exception? false :ui-timeout-millis (or ui-timeout-millis 5000) :auto-update-ui? (if-not (nil? auto-update-ui?) auto-update-ui? true) :call-tree-update? (if-not (nil? call-tree-update?) call-tree-update? true) :pprint-previews? false} :bookmarks {} :visualizers {} :data-windows {}}) ;; so linter doesn't complain (declare state) (declare fn-call-stats-map) (declare flow-thread-indexers) (defstate state :start (fn [config] (atom (initial-state config))) :stop (fn [] nil)) (defn local-mode? [] (:local-mode? @state)) (defn set-connection-status [conn-key status] (let [k ({:ws :ws-ready? :repl :repl-ready?} conn-key)] (swap! state assoc-in [:connection-status k] status))) (defn connection-status [] (get @state :connection-status)) (defn env-kind [] (let [state @state k (get-in state [:runtime-config :env-kind])] (if-not k ;; if we can determine if we are in :clj or :cljs because ;; we haven't connected to the runtime yet, lets guess from the debugger config (let [{:keys [repl]} (:debugger-config state)] (if (#{:shadow} (:repl/type repl)) :cljs :clj)) k))) (defn repl-config [] (get-in @state [:debugger-config :repl])) (defn debugger-config [] (get @state :debugger-config)) (defn set-auto-jump-on-exception [enable?] (swap! state assoc-in [:debugger-config :auto-jump-on-exception?] enable?)) (defn set-auto-update-ui [enable?] (swap! state assoc-in [:debugger-config :auto-update-ui?] enable?)) (defn set-call-tree-update [enable?] (swap! state assoc-in [:debugger-config :call-tree-update?] enable?)) (defn set-pprint-previews [enable?] (swap! state assoc-in [:debugger-config :pprint-previews?] enable?)) (defn toggle-debug-mode [] (swap! state update-in [:debugger-config :debug-mode?] not)) (defn set-selected-tool [tool] (swap! state assoc :selected-tool tool)) (defn selected-tool [] (get @state :selected-tool)) (defn set-runtime-config [config] (swap! state assoc :runtime-config config)) (defn update-thread-info [thread-id info] (swap! state assoc-in [:threads-info thread-id] info)) (defn get-threads-info [] (vals (get @state :threads-info))) (defn get-thread-info [thread-id] (get-in @state [:threads-info thread-id])) (defn get-flow [flow-id] (get-in @state [:flows flow-id])) (defn create-thread [flow-id thread-id] (swap! state assoc-in [:flows flow-id :flow/threads thread-id] {:thread/id thread-id :thread/curr-timeline-entry nil :thread/callstack-tree-hidden-fns #{} :thread/navigation-history {:head-pos 0 :history [{:fn-call-idx -1 ;; dummy entry :idx -1}]}})) (defn get-thread [flow-id thread-id] (get-in @state [:flows flow-id :flow/threads thread-id])) (defn remove-thread [flow-id thread-id] (swap! state (fn [s] (-> s (update-in [:flows flow-id :flow/threads] dissoc thread-id) (update :threads-info dissoc thread-id))))) (defn create-flow [flow-id timestamp] ;; if a flow for `flow-id` already exist we discard it and ;; will be GCed (swap! state assoc-in [:flows flow-id] {:flow/id flow-id :flow/threads {} :flow/exceptions {} :flow/exceptions-limit-reached? false :timestamp timestamp})) (defn flow-threads-ids [flow-id] (keys (get-in @state [:flows flow-id :flow/threads]))) (defn remove-flow [flow-id] (doseq [tid (flow-threads-ids flow-id)] (remove-thread flow-id tid)) (swap! state update :flows dissoc flow-id)) (defn all-flows-ids [] (keys (get @state :flows))) (defn current-timeline-entry [flow-id thread-id] (:thread/curr-timeline-entry (get-thread flow-id thread-id))) (defn current-idx [flow-id thread-id] (:idx (current-timeline-entry flow-id thread-id))) (defn set-current-timeline-entry [flow-id thread-id entry] (swap! state assoc-in [:flows flow-id :flow/threads thread-id :thread/curr-timeline-entry] entry)) (defn set-current-frame [flow-id thread-id frame-data] (swap! state assoc-in [:flows flow-id :flow/threads thread-id :thread/curr-frame] frame-data)) (defn current-frame [flow-id thread-id] (get-in @state [:flows flow-id :flow/threads thread-id :thread/curr-frame])) (defn callstack-tree-hide-fn [flow-id thread-id fn-name fn-ns] (swap! state update-in [:flows flow-id :flow/threads thread-id :thread/callstack-tree-hidden-fns] conj {:name fn-name :ns fn-ns})) (defn callstack-tree-hidden? [flow-id thread-id fn-name fn-ns] (let [hidden-set (get-in @state [:flows flow-id :flow/threads thread-id :thread/callstack-tree-hidden-fns])] (contains? hidden-set {:name fn-name :ns fn-ns}))) (defn add-printer [flow-id form-id coord printer-data] (swap! state assoc-in [:printers flow-id form-id coord] printer-data)) (defn printers [flow-id] (get-in @state [:printers flow-id])) (defn remove-printer [flow-id form-id coord] (swap! state update-in [:printers flow-id form-id] dissoc coord)) (defn update-printer [flow-id form-id coord k new-val] (swap! state assoc-in [:printers flow-id form-id coord k] new-val)) (def font-size-styles ["flowstorm/styles/font-size-sm.css" "flowstorm/styles/font-size-md.css" "flowstorm/styles/font-size-lg.css" "flowstorm/styles/font-size-xl.css"]) (defn inc-font-size [] (-> (swap! state update :selected-font-size-style-idx (fn [idx] (min (dec (count font-size-styles)) (inc idx)))) :selected-font-size-style-idx)) (defn dec-font-size [] (-> (swap! state update :selected-font-size-style-idx (fn [idx] (max 0 (dec idx)))) :selected-font-size-style-idx)) (defn set-theme [theme] (swap! state assoc :selected-theme theme)) (defn rotate-theme [] (swap! state update :selected-theme {:light :dark :dark :light})) (defn current-stylesheets [] (let [{:keys [selected-theme extra-styles selected-font-size-style-idx]} @state plugins-styles (reduce (fn [styles {:keys [plugin/css-resource plugin/dark-css-resource plugin/light-css-resource]}] (cond-> styles css-resource (conj (str (io/resource css-resource))) (and (= selected-theme :dark) dark-css-resource) (conj (str (io/resource dark-css-resource))) (and (= selected-theme :light) light-css-resource) (conj (str (io/resource light-css-resource))))) [] (plugins/plugins)) default-styles (str (io/resource "flowstorm/styles/styles.css")) theme-base-styles (str (io/resource (case selected-theme :dark "flowstorm/styles/theme_dark.css" :light "flowstorm/styles/theme_light.css"))) font-size-style (-> (get font-size-styles selected-font-size-style-idx) io/resource str) extra-styles (when extra-styles (str (io/as-url (io/file extra-styles))))] (cond-> [theme-base-styles default-styles] true (into plugins-styles) true (conj font-size-style) extra-styles (conj extra-styles)))) ;;;;;;;;;;;;;;;;;;;;;;; ;; JFX objects index ;; ;;;;;;;;;;;;;;;;;;;;;;; ;; Because scene.lookup doesn't work if you lookup before a layout pass ;; So adding a node and looking it up quickly sometimes it doesn't work ;; ui objects references with a static application lifetime ;; objects stored here will not be collected ever (defn store-obj "Store an object on an index by [flow-id, thread-id, object-id]. Ment to be used to store javafx.scene.Node objects because scene.lookup doesn't work if you lookup before a layout pass so adding a node and looking it up quickly sometimes doesn't work. More than one object can be stored under the same key. Objects stored with `store-obj` can be retrived with `obj-lookup`." ([obj-id obj-ref] (store-obj :no-flow nil obj-id obj-ref)) ([flow-id obj-id obj-ref] (store-obj flow-id nil obj-id obj-ref)) ([flow-id thread-id obj-id obj-ref] (let [k [flow-id thread-id obj-id]] (swap! state update-in [:jfx-nodes-index k] conj obj-ref)))) (defn obj-lookup "Retrieve objects stored by `store-obj`. Returns a vector of all the objects stored under the requested key." ([obj-id] (obj-lookup :no-flow nil obj-id)) ([flow-id obj-id] (obj-lookup flow-id nil obj-id)) ([flow-id thread-id obj-id] (let [k [flow-id thread-id obj-id]] (get-in @state [:jfx-nodes-index k])))) (defn clean-objs "Clear objects stored by `store-obj` under a specific flow or specific thread." ([] (clean-objs nil nil)) ([flow-id] (swap! state (fn [s] (update s :jfx-nodes-index (fn [objs] (reduce-kv (fn [ret [fid tid oid] o] (if (= fid flow-id) ret (assoc ret [fid tid oid] o))) {} objs)))))) ([flow-id thread-id] (swap! state (fn [s] (update s :jfx-nodes-index (fn [objs] (reduce-kv (fn [ret [fid tid oid] o] (if (and (= fid flow-id) (= tid thread-id)) ret (assoc ret [fid tid oid] o))) {} objs))))))) ;;;;;;;;;;;;;;; ;; Bookmarks ;; ;;;;;;;;;;;;;;; (defn add-bookmark [{:keys [flow-id thread-id idx] :as bookmark}] (swap! state assoc-in [:bookmarks [flow-id thread-id idx]] bookmark)) (defn remove-bookmark [flow-id thread-id idx] (swap! state update :bookmarks dissoc [flow-id thread-id idx])) (defn remove-bookmarks [flow-id] (swap! state update :bookmarks (fn [bookmarks] (reduce-kv (fn [bks [fid :as bkey] btext] (if (= fid flow-id) bks (assoc bks bkey btext))) {} bookmarks)))) (defn flow-bookmarks [flow-id] (->> (:bookmarks @state) vals (filter (fn [bookmark] (= flow-id (:flow-id bookmark)))))) (defn all-bookmarks [] (vals (:bookmarks @state))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Navigation undo system ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def ^:dynamic *undo-redo-jump* false) (defn update-nav-history "Add to nav history if we are jumping to a different frame, else update the head idx. If the head is not at the end, redo history will be discarded." [flow-id thread-id {:keys [fn-call-idx] :as tentry}] (swap! state update-in [:flows flow-id :flow/threads thread-id :thread/navigation-history] (fn [nav-hist] (let [{:keys [history head-pos] :as nav-hist} (if (and (not *undo-redo-jump*) (< (:head-pos nav-hist) (dec (count (:history nav-hist))))) (-> nav-hist (update :history subvec 0 (:head-pos nav-hist)) (update :head-pos dec)) nav-hist) changing-frames? (not= fn-call-idx (get-in history [head-pos :fn-call-idx]))] (if changing-frames? (-> nav-hist (update :history conj tentry) (update :head-pos inc)) (assoc-in nav-hist [:history head-pos] tentry)))))) (defn current-nav-history-entry [flow-id thread-id] (let [{:keys [history head-pos]} (get-in @state [:flows flow-id :flow/threads thread-id :thread/navigation-history])] (get history head-pos))) (defn undo-nav-history "Move the nav history head back and return it's idx." [flow-id thread-id] (swap! state update-in [:flows flow-id :flow/threads thread-id :thread/navigation-history :head-pos] (fn [p] (if (> p 1) (dec p) p))) (current-nav-history-entry flow-id thread-id)) (defn redo-nav-history "Move the nav history head forward and return it's idx." [flow-id thread-id] (swap! state update-in [:flows flow-id :flow/threads thread-id :thread/navigation-history] (fn [{:keys [history head-pos] :as h}] (assoc h :head-pos (if (< (inc head-pos) (count history)) (inc head-pos) head-pos)))) (current-nav-history-entry flow-id thread-id)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Function unwind (throws) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn maybe-add-exception "Track a limited amount of exceptions ex-data only if they doesn't already exist." [{:keys [flow-id ex-hash] :as ex-data}] (let [ex-ui-limit 100 exceptions (get-in @state [:flows flow-id :flow/exceptions]) exceptions-limit-reached? (get-in @state [:flows flow-id :flow/exceptions-limit-reached?]) ex-cnt (count exceptions)] (cond exceptions-limit-reached? :ex-limit-passed (= ex-cnt ex-ui-limit) (do (swap! state assoc-in [:flows flow-id :flow/exceptions-limit-reached?] true) :ex-limit-reached) (get exceptions ex-hash) :ex-skipped :else (do (swap! state assoc-in [:flows flow-id :flow/exceptions ex-hash] ex-data) :ex-added)))) (defn flow-exceptions [flow-id] (vals (get-in @state [:flows flow-id :flow/exceptions]))) ;;;;;;;;;;;;;;;; ;; JFX Stages ;; ;;;;;;;;;;;;;;;; (defn jfx-stages [] (get @state :jfx-stages)) (defn unregister-jfx-stage! [stg] (swap! state update :jfx-stages (fn [stgs] (filterv #(not= % stg) stgs)))) (defn main-jfx-stage [] (-> @state :jfx-stages first)) (defn reset-theming [] (let [stages (jfx-stages) new-stylesheets (current-stylesheets)] (doseq [stage stages] (let [scene (.getScene stage) scene-stylesheets (.getStylesheets scene)] (.clear scene-stylesheets) (.addAll scene-stylesheets new-stylesheets))))) (defn register-jfx-stage! [stg] (swap! state update :jfx-stages conj stg) (reset-theming)) ;;;;;;;;;;;;;;;;;;;; ;; Functions list ;; ;;;;;;;;;;;;;;;;;;;; (defn set-selected-function-list-fn [flow-id thread-id fn-call] (swap! state assoc-in [:flows flow-id :flow/threads thread-id :thread.ui/selected-functions-list-fn] fn-call)) (defn get-selected-function-list-fn [flow-id thread-id] (get-in @state [:flows flow-id :flow/threads thread-id :thread.ui/selected-functions-list-fn])) ;;;;;;;;;;;;;;;;;; ;; Data Windows ;; ;;;;;;;;;;;;;;;;;; (defn data-window-create [dw-id nodes-map] (swap! state assoc-in [:data-windows dw-id] (assoc nodes-map :stack ()))) (defn data-window [dw-id] (get-in @state [:data-windows dw-id])) (defn data-window-current-val [dw-id] (-> @state :data-windows dw-id :stack first :val-data)) (defn data-window-remove [dw-id] (swap! state update :data-windows dissoc dw-id)) (defn data-windows [] (:data-windows @state)) (defn data-window-push-frame [dw-id val-frame] (swap! state update-in [:data-windows dw-id :stack] conj val-frame)) (defn data-window-update-top-frame "Swaps the top frame of the dw-id data-window stack by new-frame-data. Returns the replaced (old top) frame." [dw-id new-frame-data] (let [prev-top (peek (:stack (data-window dw-id)))] (swap! state update-in [:data-windows dw-id :stack] (fn [stack] (conj (pop stack) (merge (peek stack) new-frame-data)))) prev-top)) (defn data-window-pop-stack-to-depth "Pop the dw-id data-window stack so it is left with depth number of elements. Returns popped elements." [dw-id depth] (let [stack (:stack (data-window dw-id)) pop-cnt (- (count stack) depth) popped (take pop-cnt stack)] (swap! state update-in [:data-windows dw-id :stack] pop-n pop-cnt) popped)) ;;;;;;;;;;; ;; Other ;; ;;;;;;;;;;; (defn clojure-storm-env? [] (get-in @state [:runtime-config :storm?])) (defn flow-storm-nrepl-middleware-available? [] (get-in @state [:runtime-config :flow-storm-nrepl-middleware?])) ================================================ FILE: src-dbg/flow_storm/debugger/tutorials/basics.clj ================================================ (ns flow-storm.debugger.tutorials.basics (:require [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.ui.utils :as ui-utils])) (def steps [ " Welcome to FlowStorm basics tutorial!

It will guide you over the basics and help you get started with FlowStorm.

If you started this repl using the command from the User's Guide QuickStart section, you find yourself in a standard (for the most) Clojure repl. You can run any expression and nothing should be different.

Nothing except single keyword evaluation.

Since evaluating single keywords on the repl is useless (they just return themselves) FlowStorm hijacks some of them as quick commands.

Try the :help command just to have a sense of what options are available, but don't focus too much on them now, we are going to cover them later.

Another command you already tried if you are here is the :dbg command, which starts the FlowStorm UI.

Any time you need the FlowStorm UI, evaluating :dbg is enough, no matter what namespace you are in, and you can always discard it by closing the window. It will take a couple of seconds the first time, but should open almost instantly from there on.

Now click on Next and I see you on the next slide!

" "

Great! Since we are going to be using the UI throughout the tutorial, let's do it comfortably.

You can toggle the debugger's UI between light/dark themes by hitting (Ctrl-t) or using the View menu which also allows you to increase/decrease the font size. You can also toggle this tutorial theme by using the button at the bottom right corner, so go ahead and set yourself comfortable first.

Now we are ready! The first thing we are going to do is to create and jump to a namespace for our tutorial. So go ahead and paste the following code in your repl :

(ns tutorial)

Next thing we need is to tell FlowStorm what namespaces to instrument, so we can record their function's executions.

We can do this by setting some JVM properties before starting the repl or by using the FlowStorm Browser (second vertical tab from the top).

For most of your projects you probably want to setup instrumentation via JVM properties, so you don't need to tell FlowStorm what to instrument every time you start your project's repl, but this time we are going to use the browser.

Click on the Browser tab and look at the instrumentations bottom panel.

There are two ways of adding instrumentation prefixes from the browser. We can find our namespace on the namespaces browser, right click on them, and then select our prefix, or we can use the Add button in the instrumentations bottom panel.

Let's find our `tutorial` namespace on the list (you can filter them using the input at the top), right click on it, and click on `Add instr prefix for tutorial.*`

A window will popup asking if we want to reload all namespaces related to our new prefix, which we can cancel since we don't have anything in our ns yet.

Reloading is important when you add a prefix for namespaces with many functions already loaded. When you reload them you will be recompiling all the functions, and because there is a instrumentation prefix for them now, the functions will compile instrumented, so you can record their execution.

These are prefixes, so this means that for the `tutorial` prefix any code compiled under `tutorial`, `tutorial.server`, `tutorial.server.core`, etc, will get instrumented. Normally adding prefixes for the top namespace of your project and some libs you are interested in will be enough.

Now we are done with instrumentation setup for this tutorial, so let's go back to the Flows vertical tab , which is called the Flows tool and is what we are going to be using for the rest of the tutorial.

The next important control to learn about is the recording button, which is the second one on the Flows tool bar. Clicking it will toggle between recording/paused. Let's leave it on pause for now (you should leave it with the circle icon), we don't want anything to be recorded yet.

On the next slide we are going to start evaluating some code.

" "

Great! Now we need some code to debug. Go ahead and evaluate the function below (you can copy and paste it on the repl) :

(defn factorial [n]
  (if (zero? n)
     1
     (* n (factorial (dec n)))))

Now if you call the function, let's say (factorial 5) you should get 120 as your result, like in any normal repl.

But let's say you now want to understand this function execution. For this you just go to the UI and put the debugger in recording mode, then run (factorial 5) again.

This time you should see the debugger UI showing the code stepping tool, which we are going to cover next.

This tool is inside a `flow-0` tab, which we are going to ignore for now, and inside a thread tab, probably called `[1] main` if you are running your repl from a terminal.

This is the thread recordings exploration tab, which contains tools for exploring this thread execution.

On the next slide we will start exploring the execution.

Tip: On your daily work you can keep recording paused and turn it on right before executing something you are interested in.

" "

The default tool is called the code stepping tool.

One thing to notice is that your factorial function code is showing there with some parts highlighted in pink, and also there is a bar at the top with some controls and some numbers.

The numbers show the position of the debugger in time for this specific thread. The number at the left is the current position, and the one on the right shows how many \"expressions executions\" were recorded so far for this thread. You can think of FlowStorm recording the activity on each thread as a timeline of execution steps, in which you can move around.

There are many ways of moving around in time in the `code stepping tool` but these are the basic ones :

  1. By using the arrows on the second row of the controls panel. They are stepping controls similar to what you can find on most debuggers, but with the extra ability to also step backwards. Check out the tooltips to know how they move and give them a try.
  2. By clicking on the highlights of the form. These are what FlowStorm captured as interesting debugging points for this frame. What it means by \"this frame\" is that clicking on a expression will take you to points in time inside the current function call. In the case of factorial that it is calling itself many times with clicks you get to navigate around the current call. You can click on any symbols and expressions. Whatever you click will get highlighted in green and the debugger will move to that point in time.

When navigating a particular call sometimes it's faster to click on the expression you want to see instead of clicking the next or prev step buttons many times.

Also notice that as you move through execution, two panels on the right change.

The top one shows a the current expression value while the bottom one shows all locals in scope.

There are two tabs for the top one, so two tools for displaying the current expression value. The first one is what FlowStorm calls a data window, which allows you to explore nested value and visualize them in different ways. The second just shows a pretty print of the value.

There is also a quick way to jump to the first execution of a function and it is by using the Quick jump box on the toolbar. It will auto complete with all the recorded functions and selecting one will take you there. It doesn't make much sense for this example since we have only one recorded function, but will be handy in more complex situations. Give it a try!

There are many more features on the code stepping tool but given this tutorial covers just the basics, we are going to skip them and jump right to another tool. The call tree tool. So go ahead and click on the second tab in the bottom left corner.

" "

Welcome to the call tree tool.

This tool will show you a expandable tree of the functions calls, which will serve as an overview of your selected thread recordings. It will be very handy when trying to understand an end to end execution, helping you create a mental model of what is going on.

Expand the one that says `(factorial 5)` and keep expanding it. This already makes evident how this recursive factorial function works by calling itself. It shows you a tree of functions calls with its arguments.

You can also click on any node and the bottom two panels will show you a pretty print of the arguments vector on the left and of the return value on the right.

Now let's say you are interested in stepping through the code of your factorial function. We can travel just before `(factorial 2)` was called. For it, you will have to expand the nodes until you see the one that is calling the function with 2, and then double click it.

It should take you to the code stepping tool with the debugger positioned right at that point in time.

You can jump between this tools using the tabs at the bottom left corner. So clicking on the third one, we are now going to learn yet another tool. The functions list tool.

" "

The functions list tool shows you all the functions next to how many times they have been called.

This is another way of looking at your recordings, which is very useful in some situations.

You should see at least a table with :

FunctionsCalls
tutorial/factorial6

This means that `tutorial/factorial` was called 6 times.

Selecting a function will show you a list of all recorded calls, together with their arguments and return values.

You can also use the checkboxes at the top to show/hide some of the arguments, which doesn't make sense on this case since we have a single argument, but can be handy in more noisy situations.

You can double click on any of the calls at the right to step over code at that specific point in time, or use the `args` or `ret` buttons to inspect the values if they have nested structure. We haven't look at the value inspector yet, but we will cover it soon.

Give it a shot, double click on the factorial call with arguments `[4]` to jump exactly to where `(factorial 4)` was called.

And that's it for the basic code exploring tools. There are more tools under the `More tools` menu but they are out of the scope of this tutorial!

Next we will learn about FlowStorm data exploring tools, so when you are ready click next.

" "

Now we are going to learn about FlowStorm value exploring and visualization capabilities, but first let's clear all recordings and try a more interesting example.

For clearing our recordings you can go to the debugger window and hit `Ctlr-L` or you can also click on the trash can on the toolbar.

This is handy in two situations. First, to get rid of old recorded data to make everything cleaner, and second, to free the recorded data so the garbage collector can get rid of it.

Note: there is a bar at the bottom right corner that will show your max heap and how much of it is currently used. You can use this to keep an eye on your heap usage so you know when to clear or stop recording.

So go ahead, clear your recordings and then evaluate the next form :

(count (all-ns))

Now click on the highlighted expression of `(all-ns)` to see this expression value.

This Clojure function returns a list of all namespaces currently loaded, and as you can see on the top right panel, it is a sequence of namespaces objects.

This panel on the top right is called a data window. There are different places in FlowStorm that allows you to open a data window to explore your values.

From the top, we have the data window id, which we are going to skip for now (take look at the User's guide for a deeper look into data windows).

The next row shows the breadcrumbs, which will allow you to navigate back as you go deeper into nested values.

The following row contains the visualizer selector, the type of the value you are looking at, the def and finally the copy button.

Expanding the visualizers drowpdown will let you select all the currently defined visualizers for your current value.

Try clicking on some of the elements to navigate into your nested values. You can also navigate back by using the top bar breadcrumbs.

Now let's introduce a very powerful feature, the `def` button. You can take whatever value you are seeing in any FlowStorm panel back to your repl by giving it a name. You do this by clicking the `def` button, and it will ask you for a name. Let's say you named it `mydata`, now you can go to your repl and find it bound to the `user/mydata` var. You can define a value for the repl in any value panel you see in FlowStorm, not just in data windows.

There is also the `copy` value, which will let you put a pretty print of the current value on your clipboard.

" "

For the last basic featur, let's see exceptions debugging.

First let's get rid of the recordings (Ctrl-L) and then eval these buggy function and call it :

(defn foo [n]
  (->> (range n)
       (filter odd?)
       (partition-all 2)
       (map second)
       (drop 10)
       (reduce +)))

(foo 70)

An exception should show on your repl! Something on the lines of :

Cannot invoke \"Object.getClass()\" because \"x\" is null

which is pretty confusing.

A red dropdown should appear at the top, showing all recorded exceptions.

Hovering over the exception will display the exception message.

You can quickly jump right before an Exception by clicking on it and then doing a step back.

You can now keep stepping backwards and try to figure out where the bug is coming from.

Give it a shot, try to see if you can figure it out!

" "

We just covered the tip of what FlowStorm can do, if you want to dig deeper the User's Guide covers all of it, like :

  1. Setting up FlowStorm in your projects
  2. Using FlowStorm with ClojureScript
  3. Using many flows
  4. Power stepping
  5. Searching
  6. Debugging loops
  7. Navigating with the stack
  8. Bookmarks
  9. Multi-thread timelines
  10. The printer tool
  11. Programmable debugging
  12. And much more!!!

You can always access the User's guide by clicking on the Help menu at the top.

Before finishing, keep in mind that even if FlowStorm is called a debugger (for lack of a better word) it was designed not just for chasing bugs, but for enhancing your interactive development experience by providing some visibility on what is going on as things execute, so it is pretty handy for other things like help you understanding a system from an execution POV or just running something and checking your assumptions.

Also here are some tips I've found for using FlowStorm efficiently :

  1. Keep recording paused when not needed
  2. Get rid of all the state (Ctrl-L) before executing the actions you are interested in recording
  3. If you know the function name you want to see use the Quick jump box to quickly jump to it's first call.
  4. If you see multiple calls, use the functions list to quickly move between them.
  5. When exploring a system you know little about, the search, power-stepping and bookmarking tools can make wrapping your head around the new system much faster.
  6. If there is any kind of looping, mapping or a function is called multiple times the printer tool is your friend.
  7. Use the jvm options described in :help to configure it, so you don't record unnecessary stuff.

And that is all for the basics. If you find any issues or suggestions feel free to open a issue in https://github.com/flow-storm/flow-storm-debugger

Now you are ready, so go and add it to your projects ! Give it a try!

Bye!

" ]) (defonce step (atom 0)) (defn step-reset [] (reset! step 0)) (defn- create-tutorial-pane [] (let [{:keys [web-view set-html]} (ui/web-view) steps-lbl (ui/label :text "") theme (atom :light) update-ui (fn [] (let [[bcolor fcolor hl-color] (case @theme :light ["#ddd" "#3f474f" "#e5b7e8"] :dark ["#323232" "#ddd" "#6c3670"]) theme-styles (format "" bcolor fcolor)] (ui-utils/set-text steps-lbl (format "%d/%d" (inc @step) (count steps))) (set-html (str "" (format "" hl-color) theme-styles "
" (get steps @step) "
" "")))) tut-controls (ui/border-pane :paddings [10] :center (ui/h-box :align :center :spacing 10 :childs [(ui/button :label "Prev" :on-click (fn [] (when (pos? @step) (swap! step dec) (update-ui)))) steps-lbl (ui/button :label "Next" :on-click (fn [] (when (< @step (dec (count steps))) (swap! step inc) (update-ui))))]) :right (ui/h-box :align :center-right :childs [(ui/button :label "Toggle theme" :on-click (fn [] (swap! theme {:light :dark, :dark :light}) (update-ui)))]))] (update-ui) (ui/border-pane :center web-view :bottom tut-controls))) (defn start-tutorials-ui [] (step-reset) (let [window-w 1000 window-h 1000 {:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) window-w window-h)] (ui/stage :scene (ui/scene :root (create-tutorial-pane) :window-width window-w :window-height window-h) :title "FlowStorm basics tutorial" :x x :y y :show? true))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/browser/screen.clj ================================================ (ns flow-storm.debugger.ui.browser.screen (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [add-class]] [flow-storm.debugger.ui.components :as ui] [flow-storm.utils :refer [log-error] :as utils] [flow-storm.debugger.state :refer [store-obj obj-lookup] :as dbg-state] [flow-storm.debugger.ui.flows.general :refer [show-message]] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.ns-reload-utils :refer [reload-all]] [clojure.string :as str])) (defn make-inst-var [var-ns var-name] {:inst-type :vanilla-var :var-ns var-ns :var-name var-name}) (defn make-inst-ns ([ns-name] (make-inst-ns ns-name nil)) ([ns-name profile] {:inst-type :vanilla-ns :profile profile :ns-name ns-name})) (defn make-inst-break [fq-fn-symb] {:inst-type :break :var-ns (namespace fq-fn-symb) :var-name (name fq-fn-symb)}) (defn make-storm-inst-only-prefix [prefix] {:inst-type :storm-inst-only-prefix :prefix prefix}) (defn make-storm-inst-skip-prefix [prefix] {:inst-type :storm-inst-skip-prefix :prefix prefix}) (defn make-storm-inst-skip-regex [regex] {:inst-type :storm-inst-skip-regex :regex regex}) (defn clear-instrumentation-list [] (let [[{:keys [clear]}] (obj-lookup "browser-observable-instrumentations-list-data")] (clear))) (defn add-to-instrumentation-list [inst] (let [[{:keys [add-all get-all-items]}] (obj-lookup "browser-observable-instrumentations-list-data") items (get-all-items) already-added? (some (fn [item] (boolean (= item inst))) items)] (when-not already-added? (add-all [inst])))) (defn remove-from-instrumentation-list [inst] (let [[{:keys [remove-all]}] (obj-lookup "browser-observable-instrumentations-list-data")] (remove-all [inst]))) (defn enable-storm-controls [] (let [[menu-btn] (obj-lookup "browser-storm-instrumentation-menu-btn")] (ui-utils/set-disable menu-btn false))) (defn update-storm-instrumentation [{:keys [instrument-only-prefixes skip-prefixes skip-regex]}] (let [[{:keys [remove-all add-all get-all-items]}] (obj-lookup "browser-observable-instrumentations-list-data") curr-items (get-all-items) new-items (cond->> (mapv make-storm-inst-only-prefix instrument-only-prefixes) true (into (mapv make-storm-inst-skip-prefix skip-prefixes)) skip-regex (into [(make-storm-inst-skip-regex skip-regex)]))] ;; clear the entries for the current prefixes (remove-all (filterv (fn [i] (#{:storm-inst-only-prefix :storm-inst-skip-prefix :storm-inst-skip-regex} (:inst-type i))) curr-items)) ;; add the updated ones (add-all new-items))) (defn- add-breakpoint [fq-fn-symb opts] (runtime-api/add-breakpoint rt-api fq-fn-symb opts)) (defn- remove-breakpoint [fq-fn-symb opts] (runtime-api/remove-breakpoint rt-api fq-fn-symb opts)) (defn- vanilla-instrument-function ([var-ns var-name] (vanilla-instrument-function var-ns var-name {})) ([var-ns var-name config] (when (or (= var-ns "clojure.core") (= var-ns "cljs.core")) (show-message "Instrumenting clojure.core is almost never a good idea since the debugger itself uses the namespace and you can easily end in a infinite recursion." :warning)) (runtime-api/vanilla-instrument-var rt-api (str var-ns) (str var-name) config))) (defn- vanilla-uninstrument-function ([var-ns var-name] (vanilla-uninstrument-function var-ns var-name {})) ([var-ns var-name config] (runtime-api/vanilla-uninstrument-var rt-api (str var-ns) (str var-name) config))) (defn- vanilla-instrument-namespaces ([namepsaces] (vanilla-instrument-namespaces namepsaces {})) ([namepsaces config] (if (some (fn [{:keys [ns-name] :as ns}] (when (= ns-name "clojure.core") ns)) namepsaces) (show-message "Instrumenting entire clojure.core is not a good idea. The debugger itself uses the namespace and you will end up in a infinite recursion" :warning) (runtime-api/vanilla-instrument-namespaces rt-api (map :ns-name namepsaces) (assoc config :profile (-> namepsaces first :profile)))))) (defn- vanilla-uninstrument-namespaces ([namespaces] (vanilla-uninstrument-namespaces namespaces {})) ([namespaces config] (runtime-api/vanilla-uninstrument-namespaces rt-api (map :ns-name namespaces) config))) (defmacro disabled-with-storm [& forms] `(if-not (dbg-state/clojure-storm-env?) (do ~@forms) (show-message "This functionality is disabled when running with ClojureStorm or ClojureScriptStorm" :warning))) (defn- update-selected-fn-detail-pane [{:keys [added ns name file static line arglists doc]}] (let [[browser-instrument-button] (obj-lookup "browser-instrument-button") [browser-instrument-rec-button] (obj-lookup "browser-instrument-rec-button") [browser-break-button] (obj-lookup "browser-break-button") [selected-fn-fq-name-label] (obj-lookup "browser-selected-fn-fq-name-label") [selected-fn-added-label] (obj-lookup "browser-selected-fn-added-label") [selected-fn-file-label] (obj-lookup "browser-selected-fn-file-label") [selected-fn-static-label] (obj-lookup "browser-selected-fn-static-label") [selected-fn-args-list-v-box] (obj-lookup "browser-selected-fn-args-list-v-box") [selected-fn-doc-label] (obj-lookup "browser-selected-fn-doc-label") args-lists-labels (map (fn [al] (ui/label :text (str al))) arglists)] (add-class browser-instrument-button "enable") (add-class browser-instrument-rec-button "enable") (add-class browser-break-button "enable") (ui-utils/set-button-action browser-instrument-button (fn [] (disabled-with-storm (vanilla-instrument-function ns name)))) (ui-utils/set-button-action browser-instrument-rec-button (fn [] (disabled-with-storm (vanilla-instrument-function ns name {:deep? true})))) (ui-utils/set-button-action browser-break-button (fn [] (add-breakpoint (symbol (str ns) (str name)) {}))) (ui-utils/set-text selected-fn-fq-name-label (format "%s" #_ns name)) (when added (ui-utils/set-text selected-fn-added-label (format "Added: %s" added))) (ui-utils/set-text selected-fn-file-label (format "File: %s:%d" file line)) (when static (ui-utils/set-text selected-fn-static-label "Static: true")) (-> selected-fn-args-list-v-box ui-utils/pane-children ui-utils/observable-clear) (ui-utils/add-childrens-to-pane selected-fn-args-list-v-box args-lists-labels) (ui-utils/set-text selected-fn-doc-label doc))) (defn- update-vars-pane [vars] (let [[{:keys [clear add-all]}] (obj-lookup "browser-observable-vars-list-data")] (clear) (add-all (sort-by :var-name vars)))) (defn- update-namespaces-pane [namespaces] (let [[{:keys [clear add-all]}] (obj-lookup "browser-observable-namespaces-list-data")] (clear) (add-all (sort namespaces)))) (defn get-var-meta [{:keys [var-name var-ns]}] (let [var-meta (runtime-api/get-var-meta rt-api var-ns var-name)] (ui-utils/run-later (update-selected-fn-detail-pane var-meta)))) (defn- get-all-vars-for-ns [ns-name] (let [all-vars (->> (runtime-api/get-all-vars-for-ns rt-api ns-name) (map (fn [vn] (make-inst-var ns-name vn))))] (ui-utils/run-later (update-vars-pane all-vars)))) (defn get-all-namespaces [] (let [all-namespaces (runtime-api/get-all-namespaces rt-api)] (ui-utils/run-later (update-namespaces-pane all-namespaces)))) (defn- modify-storm-instrumentation ([mod-data] (modify-storm-instrumentation mod-data {})) ([{:keys [inst-kind prefix regex] :as mod-data} opts] (runtime-api/modify-storm-instrumentation rt-api mod-data opts) (when (= :clj (dbg-state/env-kind)) (let [reload-regex-pattern (case inst-kind :inst-only-prefix (format "^%s.*" (str/replace prefix "." "\\.")) :inst-skip-prefix (format "^%s.*" (str/replace prefix "." "\\.")) :inst-skip-regex regex) reload? (= :apply (ui/alert-dialog :type :confirmation :width 1000 :height 100 :message (format "Do you want FlowStorm to reload all namespaces that matches #\"%s\" so the instrumentation changes take effect?" reload-regex-pattern) :buttons [:apply :cancel] :center-on-stage (dbg-state/main-jfx-stage)))] (when reload? (reload-all (re-pattern reload-regex-pattern))))))) (defn- all-prefixes [full-ns-str] (let [ns-parts (str/split full-ns-str #"\.") ns-parts-cnt (count ns-parts)] (mapv (fn [i] (->> ns-parts (take (inc i) ) (str/join "."))) (range ns-parts-cnt)))) (defn create-namespaces-pane [] (let [{:keys [list-view-pane] :as lv-data} (ui/list-view :editable? false :cell-factory (fn [list-cell ns-name] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :text ns-name)))) :on-click (fn [mev sel-items {:keys [list-view-pane]}] (when (ui-utils/mouse-secondary? mev) (let [menu-items (if (and (dbg-state/clojure-storm-env?) (= 1 (count sel-items))) (->> (all-prefixes (first sel-items)) (mapv (fn [prefix] {:text (format "Add instr prefix for %s.*" prefix) :on-click (fn [] (modify-storm-instrumentation {:inst-kind :inst-only-prefix :op :add :prefix prefix}))}))) (when-not (dbg-state/clojure-storm-env?) [{:text "Instrument namespace :light" :on-click (fn [] (vanilla-instrument-namespaces (map #(make-inst-ns % :light) sel-items)))} {:text "Instrument namespace :full" :on-click (fn [] (vanilla-instrument-namespaces (map #(make-inst-ns % :full) sel-items)))}])) ctx-menu (ui/context-menu :items menu-items)] (ui-utils/show-context-menu :menu ctx-menu :parent list-view-pane :mouse-ev mev)))) :on-selection-change (fn [_ sel-ns] (when sel-ns (get-all-vars-for-ns sel-ns))) :selection-mode :multiple :search-predicate (fn [ns-name search-str] (str/includes? ns-name search-str)))] (store-obj "browser-observable-namespaces-list-data" lv-data) list-view-pane)) (defn create-vars-pane [] (let [{:keys [list-view-pane] :as lv-data} (ui/list-view :editable? false :cell-factory (fn [list-cell {:keys [var-name]}] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :text var-name)))) :on-selection-change (fn [_ sel-var] (when sel-var (get-var-meta sel-var))) :selection-mode :single :search-predicate (fn [{:keys [var-name]} search-str] (str/includes? var-name search-str)))] (store-obj "browser-observable-vars-list-data" lv-data) list-view-pane)) (defn create-fn-details-pane [] (let [selected-fn-fq-name-label (ui/label :text "" :class "browser-fn-fq-name") inst-button (ui/button :label "Instrument" :classes ["browser-instrument-btn" "btn-sm"]) break-button (ui/button :label "Break" :classes ["browser-break-btn" "btn-sm"] :tooltip "Add a breakpoint to this function. Threads hitting this function will be paused") inst-rec-button (ui/button :label "Instrument recursively" :classes ["browser-instrument-btn" "btn-sm"]) btns-box (ui/h-box :childs [inst-button inst-rec-button break-button] :class "browser-var-buttons" :spacing 5) name-box (ui/h-box :childs [selected-fn-fq-name-label] :align :center-left) selected-fn-added-label (ui/label :text "" :class "browser-fn-attr") selected-fn-file-label (ui/label :text "" :class "browser-fn-attr") selected-fn-static-label (ui/label :text "" :class "browser-fn-attr") selected-fn-args-list-v-box (ui/v-box :childs [] :class "browser-fn-args-box") selected-fn-doc-label (ui/label :text "" :class "browser-fn-attr") selected-fn-detail-pane (ui/v-box :childs [name-box btns-box selected-fn-args-list-v-box selected-fn-added-label selected-fn-doc-label selected-fn-file-label selected-fn-static-label])] (store-obj "browser-instrument-button" inst-button) (store-obj "browser-break-button" break-button) (store-obj "browser-instrument-rec-button" inst-rec-button) (store-obj "browser-selected-fn-fq-name-label" selected-fn-fq-name-label) (store-obj "browser-selected-fn-added-label" selected-fn-added-label) (store-obj "browser-selected-fn-file-label" selected-fn-file-label) (store-obj "browser-selected-fn-static-label" selected-fn-static-label) (store-obj "browser-selected-fn-args-list-v-box" selected-fn-args-list-v-box) (store-obj "browser-selected-fn-doc-label" selected-fn-doc-label) selected-fn-detail-pane)) (defn- instrumentations-cell-factory [list-cell {:keys [inst-type] :as inst}] (try (let [inst-box (case inst-type :vanilla-var (let [{:keys [var-name var-ns]} inst inst-lbl (ui/h-box :childs [(ui/label :text "VAR INST:" :class "browser-instr-type-lbl") (ui/label :text (format "%s/%s" var-ns var-name) :class "browser-instr-label")] :spacing 10) inst-del-btn (ui/button :label "del" :classes ["browser-instr-del-btn" "btn-sm"] :on-click (fn [] (vanilla-uninstrument-function var-ns var-name)))] (ui/h-box :childs [inst-lbl inst-del-btn] :spacing 10 :align :center-left)) :vanilla-ns (let [{:keys [ns-name] :as inst-ns} inst inst-lbl (ui/h-box :childs [(ui/label :text "NS INST:" :class "browser-instr-type-lbl") (ui/label :text ns-name :class "browser-instr-label")] :spacing 10) inst-del-btn (ui/button :label "del" :classes ["browser-instr-del-btn" "btn-sm"] :on-click (fn [] (vanilla-uninstrument-namespaces [inst-ns])))] (ui/h-box :childs [inst-lbl inst-del-btn] :spacing 10 :align :center-left)) :storm-inst-only-prefix (let [{:keys [prefix]} inst inst-lbl (ui/h-box :childs [(ui/label :text "Only Prefix:" :class "browser-instr-type-lbl") (ui/label :text prefix :class "browser-instr-label")] :spacing 10) inst-del-btn (ui/button :label "del" :classes ["browser-instr-del-btn" "btn-sm"] :on-click (fn [] (modify-storm-instrumentation {:inst-kind :inst-only-prefix :op :rm :prefix prefix})))] (ui/h-box :childs [inst-lbl inst-del-btn] :spacing 10 :align :center-left)) :storm-inst-skip-prefix (let [{:keys [prefix]} inst inst-lbl (ui/h-box :childs [(ui/label :text "Skip Prefix:" :class "browser-instr-type-lbl") (ui/label :text prefix :class "browser-instr-label")] :spacing 10) inst-del-btn (ui/button :label "del" :classes ["browser-instr-del-btn" "btn-sm"] :on-click (fn [] (modify-storm-instrumentation {:inst-kind :inst-skip-prefix :op :rm :prefix prefix})))] (ui/h-box :childs [inst-lbl inst-del-btn] :spacing 10 :align :center-left)) :storm-inst-skip-regex (let [{:keys [regex]} inst inst-lbl (ui/h-box :childs [(ui/label :text "Skip Regex:" :class "browser-instr-type-lbl") (ui/label :text regex :class "browser-instr-label")] :spacing 10) inst-del-btn (ui/button :label "del" :classes ["browser-instr-del-btn" "btn-sm"] :on-click (fn [] (modify-storm-instrumentation {:inst-kind :inst-skip-regex :op :rm :regex regex})))] (ui/h-box :childs [inst-lbl inst-del-btn] :spacing 10 :align :center-left)) :break (let [{:keys [var-ns var-name]} inst inst-lbl (ui/h-box :childs [(ui/label :text "VAR BREAK:" :class "browser-instr-type-lbl") (ui/label :text (format "%s/%s" var-ns var-name) :class "browser-instr-label")] :spacing 10) inst-del-btn (ui/button :label "del" :classes ["browser-instr-del-btn" "btn-sm"] :on-click (fn [] (remove-breakpoint (symbol var-ns var-name) {})))] (ui/h-box :childs [inst-lbl inst-del-btn] :spacing 10 :align :center-left)))] (ui-utils/set-graphic list-cell inst-box)) (catch Exception e (log-error e)))) (defn- create-instrumentations-pane [] (let [{:keys [list-view-pane get-all-items] :as lv-data} (ui/list-view :editable? false :selection-mode :single :cell-factory instrumentations-cell-factory) delete-all-btn (ui/button :label "Delete all" :on-click (fn [] (let [type-groups (group-by :inst-type (get-all-items)) del-storm-inst-only-prefix (type-groups :storm-inst-only-prefix) del-storm-inst-skip-prefix (type-groups :storm-inst-skip-prefix) del-storm-inst-skip-regex (type-groups :storm-inst-skip-regex) del-vanilla-namespaces (:vanilla-ns type-groups) del-vanilla-vars (:vanilla-var type-groups) del-brks (:break type-groups)] (when (seq del-vanilla-namespaces) (vanilla-uninstrument-namespaces del-vanilla-namespaces)) (doseq [v del-vanilla-vars] (vanilla-uninstrument-function (:var-ns v) (:var-name v))) (doseq [b del-brks] (remove-breakpoint (symbol (:var-ns b) (:var-name b)) {})) (doseq [{:keys [prefix]} del-storm-inst-only-prefix] (modify-storm-instrumentation {:inst-kind :inst-only-prefix :op :rm :prefix prefix})) (doseq [{:keys [prefix]} del-storm-inst-skip-prefix] (modify-storm-instrumentation {:inst-kind :inst-skip-prefix :op :rm :prefix prefix})) (doseq [{:keys [regex]} del-storm-inst-skip-regex] (modify-storm-instrumentation {:inst-kind :inst-skip-regex :op :rm :regex regex}))))) en-dis-chk (ui/check-box :selected? true) ask-and-add-storm-inst (fn [inst-kind] (let [dialog-msg (case inst-kind :inst-only-prefix "Prefix:" :inst-skip-prefix "Prefix:" :inst-skip-regex "Regex:") operation {:inst-kind inst-kind} data (ui/ask-text-dialog :header "Modify instrumentation" :body dialog-msg :width 800 :height 200 :center-on-stage (dbg-state/main-jfx-stage))] (when-not (str/blank? data) (modify-storm-instrumentation (if (= :inst-skip-regex inst-kind) (assoc operation :regex data :op :set) (assoc operation :prefix data :op :add)))))) storm-add-inst-menu-data (ui/menu-button :title "Add" :disable? true :on-action (fn [item] (ask-and-add-storm-inst (:key item))) :items [{:key :inst-only-prefix :text "Add instrumentation prefix" :tooltip "Only namespaces that matches these prefixes will be instrumented"} {:key :inst-skip-prefix :text "Add instrumentation skip prefix" :tooltip "Use these on top of your instrumentation prefixes to skip a subset of them"} {:key :inst-skip-regex :text "Set instrumentation skip regex" :tooltip "Same as the skip prefixes but a singular regular expression"}]) instrumentations-tools (ui/h-box :childs (cond-> [(ui/label :text "Enable all") en-dis-chk delete-all-btn (:menu-button storm-add-inst-menu-data)]) :class "browser-instr-tools-box" :spacing 10 :align :center-left) pane (ui/v-box :childs [(ui/label :text "Instrumentations") instrumentations-tools list-view-pane])] (ui-utils/set-button-action en-dis-chk (fn [] (let [type-groups (group-by :inst-type (get-all-items)) change-namespaces (:vanilla-ns type-groups) change-vars (:vanilla-var type-groups) breakpoints (:break type-groups) storm-inst-only-prefix (type-groups :storm-inst-only-prefix) storm-inst-skip-prefix (type-groups :storm-inst-skip-prefix) storm-inst-skip-regex (type-groups :storm-inst-skip-regex) ] (when (seq change-namespaces) (if (ui-utils/checkbox-checked? en-dis-chk) (vanilla-instrument-namespaces change-namespaces {:disable-events? true}) (vanilla-uninstrument-namespaces change-namespaces {:disable-events? true}))) (doseq [v change-vars] (if (ui-utils/checkbox-checked? en-dis-chk) (vanilla-instrument-function (:var-ns v) (:var-name v) {:disable-events? true}) (vanilla-uninstrument-function (:var-ns v) (:var-name v) {:disable-events? true}))) (doseq [{:keys [var-ns var-name]} breakpoints] (if (ui-utils/checkbox-checked? en-dis-chk) (add-breakpoint (symbol var-ns var-name) {:disable-events? true}) (remove-breakpoint (symbol var-ns var-name) {:disable-events? true}))) (let [op (if (ui-utils/checkbox-checked? en-dis-chk) :add :rm)] (doseq [{:keys [prefix]} storm-inst-only-prefix] (modify-storm-instrumentation {:inst-kind :inst-only-prefix :op op :prefix prefix} {:disable-events? true})) (doseq [{:keys [prefix]} storm-inst-skip-prefix] (modify-storm-instrumentation {:inst-kind :inst-skip-prefix :op op :prefix prefix} {:disable-events? true})) (doseq [{:keys [regex]} storm-inst-skip-regex] (modify-storm-instrumentation {:inst-kind :inst-skip-regex :op op :regex regex} {:disable-events? true})))))) (store-obj "browser-observable-instrumentations-list-data" lv-data) (store-obj "browser-storm-instrumentation-menu-btn" (:menu-button storm-add-inst-menu-data)) pane)) (defn main-pane [] (let [namespaces-pane (create-namespaces-pane) vars-pane (create-vars-pane) selected-fn-detail-pane (create-fn-details-pane) inst-pane (create-instrumentations-pane) top-split-pane (ui/split :orientation :horizontal :childs [namespaces-pane vars-pane selected-fn-detail-pane] :sizes [0.3 0.6]) top-bottom-split-pane (ui/split :orientation :vertical :childs [top-split-pane inst-pane] :sizes [0.7])] top-bottom-split-pane)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/commons.clj ================================================ (ns flow-storm.debugger.ui.commons (:require [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.ui.utils :refer [set-clipboard]] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [clojure.string :as str])) (defn def-val ([val] (def-val val {:stage (dbg-state/main-jfx-stage)})) ([val {:keys [stage]}] (let [val-name (ui/ask-text-dialog :header "Def var with name. You can use / to provide a namespace, otherwise will be defined under [cljs.]user " :body "Var name :" :width 500 :height 100 :center-on-stage stage)] (when-not (str/blank? val-name) (runtime-api/def-value rt-api (symbol val-name) val))))) (defn copy-val [val-ref] (let [val-pprint (->> (runtime-api/val-pprint rt-api val-ref {:print-length 10000 :print-level 500 :pprint? true}) :val-str)] (set-clipboard val-pprint))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/components.clj ================================================ (ns flow-storm.debugger.ui.components (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [clojure.string :as str] [flow-storm.debugger.state :as dbg-state] [clojure.set :as set]) (:import [javafx.scene.control Button Menu ContextMenu Label ListView SelectionMode ListCell MenuItem CheckMenuItem ScrollPane Tab Alert ButtonType Alert$AlertType ProgressIndicator ProgressBar TextField TextArea TableView TableColumn TableCell TableRow TabPane$TabClosingPolicy TabPane$TabDragPolicy TableColumn$CellDataFeatures TabPane Tooltip MenuButton CustomMenuItem ComboBox CheckBox TextInputDialog SplitPane TreeView ToolBar MenuBar DialogPane ToggleButton] [javafx.scene.input KeyCombination$Modifier KeyCodeCombination KeyEvent KeyCode] [javafx.scene.layout HBox VBox BorderPane Priority GridPane AnchorPane Pane] [javafx.geometry Side Orientation NodeOrientation] [javafx.collections.transformation FilteredList] [javafx.beans.value ChangeListener] [javafx.beans.value ObservableValue] [javafx.scene Node Scene] [javafx.beans.property ReadOnlyDoubleProperty] [javafx.util Duration] [java.util.function Predicate] [org.kordamp.ikonli.javafx FontIcon] [javafx.collections FXCollections ObservableList] [org.fxmisc.richtext CodeArea] [org.fxmisc.flowless VirtualFlow] [javafx.stage Stage] [javafx.scene.web WebView WebEngine] [javafx.concurrent Worker$State])) (defn pane [& {:keys [childs classes]}] (let [p (Pane. (into-array Node childs))] (when classes (doseq [c classes] (.add (.getStyleClass p) c))) p)) (defn menu-item [{:keys [text on-click accel check-item? checked? disable?]}] (let [mi (if check-item? (CheckMenuItem. text) (MenuItem. text))] (.setOnAction mi (event-handler [_] (if check-item? (on-click (.isSelected mi)) (on-click)))) (when check-item? (.setSelected mi (boolean checked?))) (when disable? (.setDisable mi true)) (when accel (.setAccelerator mi (KeyCodeCombination. (:key-code accel) (into-array KeyCombination$Modifier (mapv ui-utils/mod-k->key-comb (:mods accel)))))) mi)) (defn context-menu [& {:keys [items]}] (let [cm (ContextMenu.) cm-items (mapv menu-item items)] (-> cm .getItems (.addAll ^objects (into-array Object cm-items))) cm)) (defn menu [& {:keys [label items]}] (let [menu (Menu. label) menu-items (mapv menu-item items)] (.setMnemonicParsing menu true) (-> menu .getItems (.addAll ^objects (into-array Object menu-items))) menu)) (defn icon [& {:keys [name]}] (FontIcon. ^String name)) (defn tool-tip [& {:keys [text]}] (doto (Tooltip. text) (.setShowDelay (Duration. 400)))) (defn button [& {:keys [label classes on-click disable tooltip]}] (let [b (Button. label)] (when on-click (.setOnAction b (event-handler [_] (on-click)))) (when classes (doseq [c classes] (.add (.getStyleClass b) c))) (when disable (.setDisable b true)) (when tooltip (.setTooltip b (tool-tip :text tooltip))) b)) (defn icon-button [& {:keys [icon-name classes on-click disable tooltip mirrored?]}] (let [b (Button.)] (ui-utils/update-button-icon b icon-name) (when tooltip (.setTooltip b (tool-tip :text tooltip))) (when on-click (.setOnAction b (event-handler [_] (on-click)))) (when classes (doseq [c classes] (.add (.getStyleClass b) c))) (when mirrored? (.add (.getStyleClass b) "mirrored")) (when disable (.setDisable b true)) b)) (defn v-box [& {:keys [childs class spacing align paddings]}] (let [box (VBox. ^"[Ljavafx.scene.Node;" (into-array Node childs))] (when paddings (apply (partial ui-utils/set-padding box) paddings)) (when align (.setAlignment box (ui-utils/alignment align))) (when spacing (.setSpacing box spacing)) (when class (.add (.getStyleClass box) class)) box)) (defn h-box [& {:keys [childs class spacing align paddings pref-height]}] (let [box (HBox. ^"[Ljavafx.scene.Node;" (into-array Node childs))] (when pref-height (.setPrefHeight box pref-height)) (when paddings (apply (partial ui-utils/set-padding box) paddings)) (when spacing (.setSpacing box spacing)) (when class (.add (.getStyleClass box) class)) (when align (.setAlignment box (ui-utils/alignment align))) box)) (defn toggle-button [& {:keys [label classes on-change disable]}] (let [tb (ToggleButton. label)] (when on-change (.setOnAction tb (event-handler [_] (on-change (.isSelected tb))))) (when classes (doseq [c classes] (.add (.getStyleClass tb) c))) (when disable (.setDisable tb true)) tb)) (defn border-pane [& {:keys [center bottom top left right class paddings]}] (let [bp (BorderPane.)] (when paddings (apply (partial ui-utils/set-padding bp) paddings)) (when center (.setCenter bp center)) (when top (.setTop bp top)) (when bottom (.setBottom bp bottom)) (when left (.setLeft bp left)) (when right (.setRight bp right)) (when class (.add (.getStyleClass bp) class)) bp)) (defn split [& {:keys [orientation childs sizes]}] (let [^SplitPane sp (SplitPane.)] (.setOrientation sp (case orientation :vertical (Orientation/VERTICAL) :horizontal (Orientation/HORIZONTAL))) (ui-utils/observable-add-all (.getItems sp) childs) (->> sizes (map-indexed (fn [i perc] (.setDividerPosition sp i perc))) doall) sp)) (defn label [& {:keys [text class pref-width on-click tooltip]}] (let [lbl (Label. text)] (when pref-width (.setPrefWidth lbl pref-width)) (when class (ui-utils/add-class lbl class)) (when tooltip (.setTooltip lbl (tool-tip :text tooltip))) (when on-click (.setOnMouseClicked lbl (event-handler [mev] (on-click mev)))) lbl)) (defn text-area [& {:keys [text editable? on-change class] :or {editable? true}}] (let [ta (TextArea.)] (when class (ui-utils/add-class ta class)) (when text (.setText ta text)) (when on-change (-> ta .textProperty (.addListener (proxy [ChangeListener] [] (changed [_ _ new-text] (on-change new-text)))))) (.setEditable ta editable?) ta)) (defn menu-button [& {:keys [title items disable? item-factory on-action class orientation]}] (let [mb (MenuButton. title) clear-items (fn [] (-> mb .getItems .clear)) item-factory (or item-factory (fn [{:keys [text tooltip]}] (let [^Label mi-lbl (label :text text)] (when tooltip (.setTooltip mi-lbl (tool-tip :text tooltip))) mi-lbl))) add-item (fn [{:keys [hide-on-click?] :as item}] (let [mi (CustomMenuItem. (item-factory item))] (.setHideOnClick mi (boolean hide-on-click?)) (.setOnAction mi (event-handler [_] (on-action item))) (-> mb .getItems (.add mi)))) set-items (fn [new-items] (clear-items) (doseq [item new-items] (add-item item)))] (when orientation (.setNodeOrientation mb (case orientation :left-to-right NodeOrientation/LEFT_TO_RIGHT :right-to-left NodeOrientation/RIGHT_TO_LEFT))) (when class (ui-utils/add-class mb class)) (when disable? (.setDisable mb true)) (set-items items) {:menu-button mb :set-items set-items :clear-items clear-items :add-item add-item})) (defn combo-box [& {:keys [items button-factory cell-factory on-change on-showing classes selected]}] (let [cb (ComboBox.) sel-model (.getSelectionModel cb)] (ui-utils/combo-box-set-items cb items) (ui-utils/selection-select-obj sel-model (or selected (first items))) (when (seq classes) (doseq [c classes] (ui-utils/add-class cb c))) (when on-change (-> cb .valueProperty (.addListener (proxy [ChangeListener] [] (changed [_ prev-val new-val] (on-change prev-val new-val)))))) (when cell-factory (.setCellFactory cb (proxy [javafx.util.Callback] [] (call [lv] (ui-utils/list-cell-factory (fn [^ListCell list-cell item] (.setGraphic list-cell (cell-factory cb item)))))))) (when button-factory (.setButtonCell cb (ui-utils/list-cell-factory (fn [^ListCell list-cell item] (.setGraphic list-cell (button-factory cb item)))))) (when on-showing (.setOnShowing cb (event-handler [_] (on-showing cb)))) cb)) (defn check-box [& {:keys [on-change selected? focus-traversable?]}] (let [cb (CheckBox.)] (.setFocusTraversable cb (boolean focus-traversable?)) (.setSelected cb (boolean selected?)) (when on-change (-> cb .selectedProperty (.addListener (proxy [ChangeListener] [] (changed [_ _ new-selected?] (on-change new-selected?)))))) cb)) (defn text-field [& {:keys [initial-text on-return-key on-change align prompt-text class pref-width tooltip]}] (let [tf (TextField. "")] (when tooltip (.setTooltip tf (tool-tip :text tooltip))) (when pref-width (.setPrefWidth tf pref-width)) (when prompt-text (.setPromptText tf prompt-text)) (when class (ui-utils/add-class tf class)) (when initial-text (.setText tf initial-text)) (when on-return-key (.setOnAction tf (event-handler [_] (on-return-key (.getText tf))))) (when align (.setAlignment tf (ui-utils/alignment align))) (when on-change (-> tf .textProperty (.addListener (proxy [ChangeListener] [] (changed [_ _ new-text] (on-change new-text)))))) tf)) (defn tab [& {:keys [id text graphic class content on-selection-changed tooltip]}] (let [t (Tab.)] (if graphic (.setGraphic t graphic) (.setText t text)) (.add (.getStyleClass t) class) (.setContent t content) (when id (.setId t (str id))) (when on-selection-changed (.setOnSelectionChanged t on-selection-changed)) (when tooltip (.setTooltip t (tool-tip :text tooltip))) t)) (defn alert-dialog [& {:keys [type message buttons center-on-stage width height] :or {type :none}}] (let [btn-key->btn-type {:apply ButtonType/APPLY :close ButtonType/CLOSE :cancel ButtonType/CANCEL} btn-type->btn-key (set/map-invert btn-key->btn-type) alert-type (get {:error Alert$AlertType/ERROR :confirmation Alert$AlertType/CONFIRMATION :information Alert$AlertType/INFORMATION :warning Alert$AlertType/WARNING :none Alert$AlertType/NONE} type) buttons-vec (->> buttons (mapv btn-key->btn-type) (into-array ButtonType)) alert-width (or width 700) alert-height (or height 100) alert (Alert. alert-type message buttons-vec) ^DialogPane dialog-pane (.getDialogPane alert)] (.setResizable alert true) (ui-utils/observable-add-all (.getStylesheets dialog-pane) (dbg-state/current-stylesheets)) (when center-on-stage (let [{:keys [x y]} (ui-utils/stage-center-box center-on-stage alert-width alert-height)] (doto dialog-pane (.setPrefWidth alert-width) (.setPrefHeight alert-height)) (doto alert (.setX x) (.setY y)))) (let [btn (.orElse (.showAndWait alert) nil)] (btn-type->btn-key btn)))) (defn progress-indicator [& {:keys [size]}] (doto (ProgressIndicator.) (.setPrefSize size size))) (defn progress-bar [& {:keys [width]}] (doto (ProgressBar.) (.setPrefWidth width))) (defn tab-pane [& {:keys [tabs rotate? side closing-policy drag-policy on-tab-change class] :or {closing-policy :unavailable drag-policy :fixed side :top rotate? false}}] (let [tp (TabPane.) tabs-list (.getTabs tp)] (when class (ui-utils/add-class tp class)) (doto tp (.setRotateGraphic rotate?) (.setSide (get {:left (Side/LEFT) :right (Side/RIGHT) :top (Side/TOP) :bottom (Side/BOTTOM)} side)) (.setTabClosingPolicy (get {:unavailable TabPane$TabClosingPolicy/UNAVAILABLE :all-tabs TabPane$TabClosingPolicy/ALL_TABS :selected-tab TabPane$TabClosingPolicy/SELECTED_TAB} closing-policy)) (.setTabDragPolicy (get {:fixed TabPane$TabDragPolicy/FIXED :reorder TabPane$TabDragPolicy/REORDER} drag-policy))) (when on-tab-change (-> tp .getSelectionModel .selectedItemProperty (.addListener (proxy [ChangeListener] [] (changed [_ old-tab new-tab] (on-tab-change old-tab new-tab)))))) (when tabs (.addAll tabs-list ^objects (into-array Object tabs))) tp)) (defn scroll-pane [& {:keys [class]}] (let [sp (ScrollPane.)] (when class (ui-utils/add-class sp class)) sp)) (defn anchor-pane [& {:keys [childs]}] (let [ap (AnchorPane.)] (doseq [{:keys [top-anchor left-anchor right-anchor bottom-anchor node]} childs] (when top-anchor (AnchorPane/setTopAnchor node top-anchor)) (when left-anchor (AnchorPane/setLeftAnchor node left-anchor)) (when right-anchor (AnchorPane/setRightAnchor node right-anchor)) (when bottom-anchor (AnchorPane/setBottomAnchor node bottom-anchor))) (-> ap .getChildren (.addAll (mapv :node childs))) ap)) (defn list-view [& {:keys [editable? cell-factory on-click on-enter on-selection-change selection-mode search-predicate] :or {editable? false selection-mode :multiple}}] (let [observable-list (FXCollections/observableArrayList) ;; by default create a constantly true predicate observable-filtered-list (FilteredList. observable-list) lv (ListView. observable-filtered-list) list-selection (.getSelectionModel lv) add-all (fn [elems] (.addAll observable-list ^objects (into-array Object elems))) remove-all (fn [elems] (.removeAll observable-list ^objects (into-array Object elems))) clear (fn [] (.clear observable-list)) get-all-items (fn [] (seq observable-list)) selected-items (fn [] (.getSelectedItems list-selection)) search-field (TextField.) search-bar (h-box :childs [(doto (Label.) (.setGraphic (icon :name "mdi-magnify"))) search-field]) box (v-box :childs (cond-> [] search-predicate (conj search-bar) true (conj lv))) list-view-data {:list-view-pane box :list-view lv :add-all add-all :clear clear :get-all-items get-all-items :remove-all remove-all}] (HBox/setHgrow search-field Priority/ALWAYS) (HBox/setHgrow lv Priority/ALWAYS) (VBox/setVgrow lv Priority/ALWAYS) (when search-predicate (.addListener (.textProperty search-field) (proxy [ChangeListener] [] (changed [_ _ new-val] (.setPredicate observable-filtered-list (proxy [Predicate] [] (test [item] (search-predicate item new-val)))))))) (case selection-mode :multiple (.setSelectionMode list-selection SelectionMode/MULTIPLE) :single (.setSelectionMode list-selection SelectionMode/SINGLE)) (when cell-factory (.setCellFactory lv (proxy [javafx.util.Callback] [] (call [lv] (ui-utils/list-cell-factory cell-factory))))) (.setEditable lv editable?) (when on-enter (.setOnKeyPressed lv (event-handler [^KeyEvent kev] (when (= "Enter" (.getName (.getCode kev))) (on-enter (selected-items)))))) (when on-click (.setOnMouseClicked lv (event-handler [mev] (on-click mev (selected-items) list-view-data)))) (when on-selection-change (-> list-selection .selectedItemProperty (.addListener (proxy [ChangeListener] [] (changed [_ old-val new-val] (on-selection-change old-val new-val)))))) list-view-data)) (defn table-view [& {:keys [columns cell-factory row-update items selection-mode search-predicate on-click on-enter resize-policy columns-width-percs on-selection-change] :or {selection-mode :multiple resize-policy :unconstrained}}] (assert (every? #(= (count %) (count columns)) items) "Every item should have the same amount of elements as columns") (let [^TableView tv (TableView.) make-column (fn [col-idx col-text] (let [col (doto (TableColumn. col-text) (.setCellValueFactory (proxy [javafx.util.Callback] [] (call [^TableColumn$CellDataFeatures cell-val] (proxy [ObservableValue] [] (addListener [_]) (removeListener [_]) (getValue [] (get (.getValue cell-val) col-idx)))))) (.setCellFactory (proxy [javafx.util.Callback] [] (call [tcol] (proxy [TableCell] [] (updateItem [item empty?] (let [^TableCell this this] (proxy-super updateItem item empty?) (if empty? (doto this (.setGraphic nil) (.setText nil)) (let [cell-graphic (cell-factory this item)] (doto this (.setText nil) (.setGraphic cell-graphic)))))))))))] (when (seq columns-width-percs) (.bind (.prefWidthProperty col) (let [^ReadOnlyDoubleProperty wp (.widthProperty tv)] (.multiply wp ^double (get columns-width-percs col-idx))))) col)) columns (map-indexed make-column columns) table-selection (.getSelectionModel tv) selected-items (fn [] (.getSelectedItems table-selection)) search-field (TextField.) search-bar (h-box :childs [(doto (Label.) (.setGraphic (icon :name "mdi-magnify"))) search-field]) box (v-box :childs (cond-> [] search-predicate (conj search-bar) true (conj tv))) ^ObservableList items-array-list (FXCollections/observableArrayList ^objects (into-array Object items)) filtered-items-array (FilteredList. items-array-list) add-all (fn [elems] (.addAll items-array-list ^objects (into-array Object elems))) clear (fn [] (.clear items-array-list)) table-data {:table-view tv :table-view-pane box :clear clear :add-all add-all}] (when row-update (.setRowFactory tv (proxy [javafx.util.Callback] [] (call [tcol] (proxy [TableRow] [] (updateItem [item empty?] (let [^TableRow this this] (proxy-super updateItem item empty?) (row-update this item)))))))) (when on-selection-change (-> table-selection .selectedItemProperty (.addListener (proxy [ChangeListener] [] (changed [_ old-val new-val] (on-selection-change old-val new-val)))))) (.clear (.getColumns tv)) (.addAll (.getColumns tv) ^objects (into-array Object columns)) (.setItems tv filtered-items-array) (.setColumnResizePolicy tv (case resize-policy :unconstrained TableView/UNCONSTRAINED_RESIZE_POLICY :constrained TableView/CONSTRAINED_RESIZE_POLICY)) (HBox/setHgrow search-field Priority/ALWAYS) (HBox/setHgrow tv Priority/ALWAYS) (VBox/setVgrow tv Priority/ALWAYS) (case selection-mode :multiple (.setSelectionMode table-selection SelectionMode/MULTIPLE) :single (.setSelectionMode table-selection SelectionMode/SINGLE)) (when search-predicate (.addListener (.textProperty search-field) (proxy [ChangeListener] [] (changed [_ _ new-val] (.setPredicate filtered-items-array (proxy [Predicate] [] (test [item] (search-predicate item new-val)))))))) (when on-enter (.setOnKeyPressed tv (event-handler [^KeyEvent kev] (when (= "Enter" (.getName ^KeyCode (.getCode kev))) (on-enter (selected-items)))))) (when on-click (.setOnMouseClicked tv (event-handler [mev] (on-click mev (selected-items) table-data)))) table-data)) (defn tree-view [& {:keys [cell-factory editable?]}] (let [tv (TreeView.)] (when cell-factory (.setCellFactory tv cell-factory)) (.setEditable tv (boolean editable?)) tv)) (defn autocomplete-textfield "Creates a textfield with autocompletion. `get-completions` should be a fn that will be called with no args when the text length changes from 0 to 1 which should to retrieve the autocompletion list as a collection of {:text \"...\" :on-select (fn [] ..)} objects. A max of 40 items is displayed. When `on-select-set-text?` is true, selection will will set the selected :text as the value of the text field. Returns a TextField." [& {:keys [get-completions on-select-set-text?]}] (let [^TextField tf (TextField.) ^ContextMenu options-menu (ContextMenu.) options (atom nil)] (.addListener (.textProperty tf) (proxy [ChangeListener] [] (changed [_ old-val new-val] (when (= 0 (count new-val)) (reset! options nil)) (when (and (= 0 (count old-val)) (pos? (count new-val))) (reset! options (get-completions))) (let [new-items (reduce (fn [r {:keys [text on-select]}] (if (< (count r) 40) (if (str/includes? text new-val) (conj r (doto (MenuItem. text) (.setOnAction (event-handler [_] (.setText tf (if on-select-set-text? text "")) (on-select))))) r) (reduced r))) [] @options) ^ObservableList menu-items (.getItems options-menu)] (.clear menu-items) (if (seq new-items) (do (.addAll menu-items ^objects (into-array Object new-items)) (.show options-menu tf Side/BOTTOM 0 0)) (.hide options-menu)))))) tf)) (defn code-area [& {:keys [editable? text]}] (let [ca (proxy [CodeArea] [] (computePrefHeight [w] (let [^CodeArea this this ih (+ (-> this .getInsets .getTop) (-> this .getInsets .getBottom)) ^ObservableList childs (.getChildren this) p-cnt (.size (.getParagraphs this))] (if (and (pos? (.size childs)) (pos? p-cnt)) (let [^VirtualFlow c (.get childs 0)] (+ ih (->> (range p-cnt) (map (fn [i] (-> c (.getCell i) .getNode (.prefHeight w)))) (reduce + 0)))) ;; else (+ ih (.getTotalHeightEstimate this))))))] (doto ca (.setEditable editable?) (.replaceText 0 0 text)))) (defn ask-text-dialog [& {:keys [header body width height center-on-stage]}] (let [^TextInputDialog tdiag (doto (TextInputDialog.) (.setHeaderText header) (.setContentText body)) dialog-pane (.getDialogPane tdiag)] (ui-utils/observable-add-all (.getStylesheets dialog-pane) (dbg-state/current-stylesheets)) (when (and width height) (.setPrefWidth dialog-pane width) (.setPrefHeight dialog-pane height)) (when (and width height center-on-stage) (let [{:keys [x y]} (ui-utils/stage-center-box center-on-stage width height)] (.setX tdiag x) (.setY tdiag y))) (.showAndWait tdiag) (-> tdiag .getEditor .getText))) (defn ask-text-and-bool-dialog [& {:keys [header body width height center-on-stage bool-msg]}] (let [^TextInputDialog tdiag (doto (TextInputDialog.) (.setHeaderText header) (.setContentText body)) checkb (CheckBox.) dialog-pane (.getDialogPane tdiag)] (ui-utils/observable-add-all (.getStylesheets dialog-pane) (dbg-state/current-stylesheets)) (when bool-msg (.add ^GridPane (.getContent ^DialogPane (.getDialogPane tdiag)) (label :text bool-msg) 0 2) (.add ^GridPane (.getContent ^DialogPane (.getDialogPane tdiag)) checkb 1 2)) (when (and width height) (.setPrefWidth dialog-pane width) (.setPrefHeight dialog-pane height)) (when (and width height center-on-stage) (let [{:keys [x y]} (ui-utils/stage-center-box center-on-stage width height)] (.setX tdiag x) (.setY tdiag y))) (.showAndWait tdiag) {:text (-> tdiag .getEditor .getText) :bool (ui-utils/checkbox-checked? checkb)})) (defn ask-text-dialog+ "bodies should be like : [{:key :name :label \"Name\" :init-text \"\"} {:key :age :label \"Age\" :init-text \"42\"}] returns like : {:name \"John\" :age \"42\"}" [& {:keys [header bodies width height center-on-stage]}] ;; (let [^TextInputDialog tdiag (doto (TextInputDialog.) (.setHeaderText header)) bodies' (mapv (fn [{:keys [init-text tooltip] :as b} row] (assoc b :row row :text-field (text-field :initial-text init-text :tooltip tooltip))) bodies (range)) dialog-pane (.getDialogPane tdiag)] (ui-utils/observable-add-all (.getStylesheets dialog-pane) (dbg-state/current-stylesheets)) (doseq [b bodies'] (.add ^GridPane (.getContent ^DialogPane (.getDialogPane tdiag)) (label :text (:label b)) 0 (:row b)) (.add ^GridPane (.getContent ^DialogPane (.getDialogPane tdiag)) (:text-field b) 1 (:row b))) (when (and width height) (.setPrefWidth dialog-pane width) (.setPrefHeight dialog-pane height)) (when (and width height center-on-stage) (let [{:keys [x y]} (ui-utils/stage-center-box center-on-stage width height)] (.setX tdiag x) (.setY tdiag y))) (.showAndWait tdiag) (reduce (fn [acc b] (assoc acc (:key b) (.getText (:text-field b)))) {} bodies'))) (defn thread-label [thread-id thread-name] (if thread-name (format "(%d) %s" thread-id thread-name) (format "thread-%d" thread-id))) (defn toolbar [& {:keys [childs]}] (let [tb (ToolBar. (into-array Node childs))] tb)) (defn menu-bar [& {:keys [menues]}] (let [^MenuBar mb (MenuBar.)] (ui-utils/observable-add-all (.getMenus mb) menues) mb)) (defn scene [& {:keys [root window-width window-height]}] (Scene. root window-width window-height)) (defn stage [& {:keys [scene title on-close-request x y show?]}] (let [^Stage stg (doto (Stage.) (.setScene scene) (.setTitle title) (.setX x) (.setY y))] (.setOnCloseRequest stg (event-handler [_] (dbg-state/unregister-jfx-stage! stg) (when on-close-request (on-close-request)))) (dbg-state/register-jfx-stage! stg) (when show? (.show stg)) stg)) (defn web-view [] (let [^WebView wv (WebView.) ^WebEngine web-engine (.getEngine wv)] {:web-view wv :set-html (fn [html] (.loadContent web-engine html)) :load-url (fn [url] (.load web-engine url)) :set-handlers (fn [handlers-map] (-> web-engine .getLoadWorker .stateProperty (.addListener (proxy [ChangeListener] [] (changed [_ old-state new-state] (when (= new-state Worker$State/SUCCEEDED) (let [window (.executeScript web-engine "window")] (doseq [[method-name method-fn] handlers-map] (.setMember window method-name method-fn)))))))))})) ================================================ FILE: src-dbg/flow_storm/debugger/ui/data_windows/data_windows.clj ================================================ (ns flow-storm.debugger.ui.data-windows.data-windows (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui] [flow-storm.utils :as utils :refer [log-error]] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.commons :refer [copy-val def-val]] [flow-storm.debugger.ui.data-windows.visualizers :as visualizers]) (:import [javafx.scene Scene] [javafx.stage Stage] [javafx.scene.layout Priority VBox HBox])) (defn data-window-pane [{:keys [data-window-id]}] (let [breadcrums-box (ui/h-box :childs [] :spacing 10 :class "breadcrums") visualizers-combo-box (ui/h-box :childs []) val-box (ui/v-box :childs [] :spacing 10 :paddings [10 0 0 0]) type-lbl (ui/label :text "") def-btn (ui/button :label "def" :classes ["def-btn" "btn-sm"] :tooltip "Define a reference to this value so it can be used from the repl.") copy-btn (ui/icon-button :icon-name "mdi-content-copy" :tooltip "Copy the pprint of the current value" :classes ["btn-sm"]) val-pane (ui/border-pane :top (ui/h-box :childs [visualizers-combo-box type-lbl def-btn copy-btn] :align :center-left :spacing 5) :center val-box) dw-box (ui/v-box :childs [(ui/label :text (format "Data Window id: %s" data-window-id)) breadcrums-box val-pane] :class "data-window" :spacing 10 :paddings [10 10 10 10])] (dbg-state/data-window-create data-window-id {:breadcrums-box breadcrums-box :visualizers-combo-box visualizers-combo-box :val-box val-box :type-lbl type-lbl :def-btn def-btn :copy-btn copy-btn}) (VBox/setVgrow val-pane Priority/ALWAYS) (HBox/setHgrow val-pane Priority/ALWAYS) (VBox/setVgrow val-box Priority/ALWAYS) (HBox/setHgrow val-box Priority/ALWAYS) (VBox/setVgrow dw-box Priority/ALWAYS) (HBox/setHgrow dw-box Priority/ALWAYS) dw-box)) (defn- destroy-visualizers-for-frames [frames] (doseq [fr frames] (when-let [on-destroy (-> fr :visualizer :on-destroy)] (try (on-destroy (-> fr :visualizer-val-ctx)) (catch Exception e (utils/log-error (str "Couldn't destroy visualizer " (:visualizer fr)) e)))))) (defn- create-data-window [dw-id] (try (let [inspector-w 1000 inspector-h 600 stage (doto (Stage.) (.setTitle "FlowStorm data window")) scene (Scene. (data-window-pane {:data-window-id dw-id}) inspector-w inspector-h)] (.setScene stage scene) (.setOnCloseRequest stage (event-handler [_] (dbg-state/unregister-jfx-stage! stage) (destroy-visualizers-for-frames (:stack (dbg-state/data-window dw-id))) (dbg-state/data-window-remove dw-id))) (dbg-state/register-jfx-stage! stage) (let [{:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) inspector-w inspector-h)] (.setX stage x) (.setY stage y)) (-> stage .show)) (catch Exception e (log-error "UI Thread exception" e)))) (defn create-data-window-for-vref [vref] (let [dw-id (keyword (str "value-" (:vid vref)))] (create-data-window dw-id) (runtime-api/data-window-push-val-data rt-api dw-id vref {:stack-key "/" :dw-id dw-id}))) (defn push-val ([dw-id val-data] (push-val dw-id val-data {})) ([dw-id val-data {:keys [root? visualizer preferred-size stack-key]}] (ui-utils/run-now ;; this is to allow a data-window to be created by a push from the runtime (when-not (dbg-state/data-window dw-id) (create-data-window dw-id)) (let [{:keys [breadcrums-box visualizers-combo-box val-box type-lbl def-btn copy-btn]} (dbg-state/data-window dw-id) visualizers (visualizers/appliable-visualizers val-data) reset-val-box (fn [] (let [val-node (-> (dbg-state/data-window dw-id) :stack peek :visualizer-val-ctx :fx/node) {:flow-storm.runtime.values/keys [meta-ref type val-ref]} (-> (dbg-state/data-window dw-id) :stack peek :val-data) meta-preview (-> meta-ref meta :val-preview)] (ui-utils/set-button-action def-btn (fn [] (def-val val-ref))) (ui-utils/set-button-action copy-btn (fn [] (copy-val val-ref))) (ui-utils/set-text type-lbl type) (VBox/setVgrow val-node Priority/ALWAYS) (HBox/setHgrow val-node Priority/ALWAYS) (ui-utils/observable-clear (.getChildren val-box)) (ui-utils/observable-add-all (.getChildren val-box) (cond->> [val-node] meta-ref (into [(ui/label :text (format "Meta: %s" meta-preview) :class "link-lbl" :on-click (fn [_] (let [extras {:dw-id dw-id :stack-key "META"}] (runtime-api/data-window-push-val-data rt-api dw-id meta-ref extras))))]))))) reset-viz-combo (fn [] (let [viz-combo (-> (dbg-state/data-window dw-id) :stack peek :visualizer-combo)] (ui-utils/observable-clear (.getChildren visualizers-combo-box)) (ui-utils/observable-add-all (.getChildren visualizers-combo-box) [viz-combo]))) default-viz (or (and visualizer (visualizers/visualizer visualizer)) (visualizers/default-visualizer val-data) (first visualizers)) create-viz (fn [{:keys [on-create]}] (try (on-create (cond-> val-data true (assoc ::dw-id dw-id) preferred-size (assoc ::preferred-size preferred-size))) (catch Exception e {:fx/node (ui/text-area :text (pr-str e) :editable? false :class "monospaced")}))) viz-combo (ui/combo-box :items visualizers :selected default-viz :cell-factory (fn [_ {:keys [id]}] (ui/label :text (str id))) :button-factory (fn [_ {:keys [id]}] (ui/label :text (str id))) :on-change (fn [_ new-visualizer] (let [new-visualizer-val-ctx (create-viz new-visualizer) old-frame (dbg-state/data-window-update-top-frame dw-id {:visualizer new-visualizer :visualizer-val-ctx new-visualizer-val-ctx})] (destroy-visualizers-for-frames [old-frame]) (reset-val-box)))) reset-breadcrums (fn reset-breadcrums [] (let [stack (:stack (dbg-state/data-window dw-id)) bbox-childs (.getChildren breadcrums-box)] (ui-utils/observable-clear bbox-childs) (let [btns (->> stack (map-indexed (fn [idx frame] (let [depth (- (count stack) idx)] (doto (ui/button :label (or (:stack-key frame) (format "unnamed-frame-<%d>" depth)) :on-click (fn [] (let [popped-frames (dbg-state/data-window-pop-stack-to-depth dw-id depth)] (destroy-visualizers-for-frames popped-frames) (reset-breadcrums) (reset-viz-combo) (reset-val-box)))) (.setMaxWidth 120))))) reverse (into []))] (ui-utils/observable-add-all bbox-childs btns)))) default-viz-val-ctx (create-viz default-viz)] (when root? (let [popped-frames (dbg-state/data-window-pop-stack-to-depth dw-id 0)] (destroy-visualizers-for-frames popped-frames))) (dbg-state/data-window-push-frame dw-id {:val-data val-data :stack-key stack-key :visualizer-combo viz-combo :visualizer default-viz :visualizer-val-ctx default-viz-val-ctx}) (reset-breadcrums) (reset-viz-combo) (reset-val-box))))) (defn update-val [dw-id update-data] (let [{:keys [stack]} (dbg-state/data-window dw-id) {:keys [val-data visualizer visualizer-val-ctx]} (peek stack) {:keys [on-update]} visualizer] (when on-update (ui-utils/run-later (on-update val-data visualizer-val-ctx update-data))))) (comment (ui-utils/run-later (create-data-window :data-1)) ;; stack (push-val :data-1 {:flow-storm.runtime.values/kind :map :preview-str "{:a 10}" :keys-previews [":a"] :keys-refs [10] :vals-previews ["10"] :vals-refs [15]}) (push-val :data-1 {:flow-storm.runtime.values/kind :vec :stack-key ":hello" :preview-str "[1 2 3 4]" :vals-previews ["1" "2" "3" "4"] :vals-refs [20 21 22 23]}) (push-val :data-1 {:flow-storm.runtime.values/kind :string :preview-str "hello world" :val-ref 55}) ;; lazy sequence (ui-utils/run-later (create-data-window :data-2)) (push-val :data-2 {:flow-storm.runtime.values/kind :lazy-seq :preview-str "(1 2 3 ...)" :page-previews ["1" "2" "3"] :page-refs [61 62 63] :more? true :next-ref 77 }) (update-val :data-2 {:page-previews ["4" "5" "6"] :page-refs [64 65 66] :more? true :next-ref 88}) (update-val :data-2 {:page-previews ["7" "8" "9"] :page-refs [67 68 69] :more? false}) ;; scope (ui-utils/run-later (create-data-window :data-3)) (push-val :data-3 {:flow-storm.runtime.values/kind :number :val 0}) (dotimes [i 5] (update-val :data-3 {:new-val (inc i)}) (Thread/sleep 1000)) (:stack (dbg-state/data-window :data-1)) ) ================================================ FILE: src-dbg/flow_storm/debugger/ui/data_windows/visualizers/oscilloscope.clj ================================================ (ns flow-storm.debugger.ui.data-windows.visualizers.oscilloscope (:require [flow-storm.debugger.ui.components :as ui] [flow-storm.runtime.values :refer [sample-chan-1 sample-chan-2 frame-samples frame-samp-rate]] [flow-storm.debugger.ui.utils :as ui-utils]) (:import [javafx.scene.canvas Canvas GraphicsContext] [javafx.animation AnimationTimer] [javafx.scene.paint Color] [javafx.scene.text Font] [java.util.concurrent.locks ReentrantLock])) (set! *warn-on-reflection* true) (defprotocol VariableSamplesRingP (put-sample [_ obj]) (get-samples [_]) (reset-limit [_ limit]) (get-limit [_])) (deftype VariableSamplesRing [arr ^:unsynchronized-mutable ^:int limit ^:unsynchronized-mutable ^:int write-head ^:unsynchronized-mutable ^:double min-mag ^:unsynchronized-mutable ^:double max-mag lock] VariableSamplesRingP (put-sample [_ obj] (.lock ^ReentrantLock lock) (aset ^objects arr write-head obj) (set! min-mag (min min-mag (sample-chan-1 obj) (sample-chan-2 obj))) (set! max-mag (max max-mag (sample-chan-1 obj) (sample-chan-2 obj))) (set! write-head (if (< write-head limit) (inc write-head) 0)) (.unlock ^ReentrantLock lock)) (get-samples [_] (let [objs-array (object-array limit)] (.lock ^ReentrantLock lock) (System/arraycopy arr write-head objs-array 0 (- limit write-head)) (System/arraycopy arr 0 objs-array (- limit write-head) write-head) (.unlock ^ReentrantLock lock) {:sample-objects (vec objs-array) :min-mag min-mag :max-mag max-mag})) (reset-limit [_ l] (.lock ^ReentrantLock lock) (set! limit (min l (count arr))) (set! write-head 0) (.unlock ^ReentrantLock lock)) (get-limit [_] limit)) (defn make-variable-samples-ring [capacity] (VariableSamplesRing. (object-array capacity) capacity 0 Long/MAX_VALUE Long/MIN_VALUE (ReentrantLock.))) (def amp-per-div [0.001 0.002 0.05 0.1 0.2 0.5 1 2 5 10 20 50 100]) (def nanos-per-div [5 10 20 50 100 200 500 ;; nano 1e3 2e3 5e3 10e3 20e3 50e3 100e3 200e3 500e3 ;; micro 1e6 2e6 5e6 10e6 20e6 50e6 100e6 200e6 500e6 ;; milli 1e9 2e9 ;; sec ]) (def format-nanos {5 "5ns", 10 "10ns", 20 "20ns", 50 "50ns", 100 "100ns", 200 "200ns", 500 "500ns", 1e3 "1us", 2e3 "2us", 5e3 "5us", 10e3 "10us", 20e3 "20us", 50e3 "50us", 100e3 "100us", 200e3 "200us", 500e3 "500us", 1e6 "1ms", 2e6 "2ms", 5e6 "5ms", 10e6 "10ms", 20e6 "20ms", 50e6 "50ms", 100e6 "100ms", 200e6 "200ms", 500e6 "500ms", 1e9 "1s", 2e9 "2s"}) (defn needed-window-size [frames-samp-rate nanos-per-div divisions] (let [nanos-per-sample (/ 1e9 frames-samp-rate)] (int (* (/ nanos-per-div nanos-per-sample) divisions)))) (def grid-vert-divs 10) (def grid-horiz-divs 7) (defn draw-anim-frame [^GraphicsContext gc margins canvas-width canvas-height samples-ring nanos-per-sample *nanos-per-div-idx *amp-per-div-idx *v-offset] (try (let [draw-origin margins draw-width (- canvas-width (* 2 margins)) draw-height (- canvas-height (* 2 margins)) mid-y (+ margins (/ draw-height 2)) grid-div-height (/ draw-height grid-horiz-divs) {:keys [sample-objects]} (get-samples samples-ring) grid-div-width (/ draw-width grid-vert-divs) samples-per-div (/ (get nanos-per-div @*nanos-per-div-idx) nanos-per-sample) samples-objects-cnt (count sample-objects) x-step (/ grid-div-width samples-per-div) selected-amp-per-div (get amp-per-div @*amp-per-div-idx) v-offset @*v-offset] (.setFill gc Color/BLUEVIOLET) (.fillRect gc 0 0 canvas-width canvas-height) (.clearRect gc draw-origin draw-origin draw-width draw-height) ;; grid (.setStroke gc Color/GRAY) ;; grid vert lines (dotimes [i (inc grid-vert-divs)] (let [div-x (* i grid-div-width)] (.strokeLine gc (+ margins div-x) draw-origin (+ margins div-x) (+ margins draw-height)))) ;; grid horiz lines (loop [i 0] (when (<= i grid-horiz-divs) (let [div-y (+ margins (* i grid-div-height))] (.strokeLine gc draw-origin div-y (+ margins draw-width) div-y)) (recur (inc i)))) ;; draw zero marker (let [zero-y (-> (+ v-offset mid-y) (min (+ margins draw-height)) (max margins))] (.setFill gc Color/GREENYELLOW) (.fillPolygon gc (double-array [margins (- margins 10) (- margins 10)]) (double-array [zero-y (- zero-y 10) (+ zero-y 10)]) 3)) ;; samples (let [to (min (dec samples-objects-cnt) (* samples-per-div grid-vert-divs))] (loop [i 0 x margins] (when (< i to) (let [x-next (+ x x-step) i-next (inc i) samp-obj (get sample-objects i) samp-obj-next (get sample-objects (inc i))] (when (and samp-obj samp-obj-next) (let [ch-1s (sample-chan-1 samp-obj) ch-1s-next (sample-chan-1 samp-obj-next) ch-2s (sample-chan-2 samp-obj) ch-2s-next (sample-chan-2 samp-obj-next) ch-1s-y (+ v-offset (- mid-y (/ (* grid-div-height ch-1s) selected-amp-per-div))) ch-1s-next-y (+ v-offset (- mid-y (/ (* grid-div-height ch-1s-next) selected-amp-per-div))) ch-2s-y (+ v-offset (- mid-y (/ (* grid-div-height ch-2s) selected-amp-per-div))) ch-2s-next-y (+ v-offset (- mid-y (/ (* grid-div-height ch-2s-next) selected-amp-per-div)))] (.setStroke gc Color/BLUE) (.strokeLine ^GraphicsContext gc x ch-1s-y x-next ch-1s-next-y) (.setStroke gc Color/GREEN) (.strokeLine ^GraphicsContext gc x ch-2s-y x-next ch-2s-next-y))) (recur i-next x-next)))))) (catch Exception e (.printStackTrace e)))) (defn oscilloscope-create [data] (let [first-frame (:frame data) preferred-size (:flow-storm.debugger.ui.data-windows.data-windows/preferred-size data) [canvas-width canvas-height margins] (if (= :small preferred-size) [400 280 25] [1000 700 25]) *capturing (atom true) *nanos-per-div-idx (atom 17) *amp-per-div-idx (atom 6) *v-offset (atom 0) max-capture-size 12e6 ;; max of 12 million samples samples-ring (make-variable-samples-ring max-capture-size) canvas (Canvas. canvas-width canvas-height) ^GraphicsContext gc (.getGraphicsContext2D canvas) _ (.setFont gc (Font. "Arial" 20)) _ (.setFill gc Color/MAGENTA) *curr-samp-rate (atom nil) anim-timer (proxy [AnimationTimer] [] (handle [^long now] (let [nanos-per-sample (/ 1e9 @*curr-samp-rate)] (draw-anim-frame gc margins canvas-width canvas-height samples-ring nanos-per-sample *nanos-per-div-idx *amp-per-div-idx *v-offset)))) samp-rate-lbl (ui/label :text "") nanos-per-div-lbl (ui/label :text "") capture-samples-lbl (ui/label :text "") units-per-vert-div-lbl (ui/label :text "") refresh-settings (fn [] (let [selected-nanos (get nanos-per-div @*nanos-per-div-idx) curr-samp-rate @*curr-samp-rate needed-ws (needed-window-size curr-samp-rate selected-nanos grid-vert-divs) capture-samples (max (min needed-ws max-capture-size) 0)] (reset-limit samples-ring capture-samples) (ui-utils/set-text samp-rate-lbl (str curr-samp-rate " samps/sec")) (ui-utils/set-text capture-samples-lbl (str "Samples:" capture-samples)) (ui-utils/set-text nanos-per-div-lbl (str "H: " (format-nanos selected-nanos))) (ui-utils/set-text units-per-vert-div-lbl (str "V: " (get amp-per-div @*amp-per-div-idx))))) add-frame (fn add-frame [frame] (when @*capturing ;; allow for sample rates to change between frames (let [frame-rate (frame-samp-rate frame) curr-rate @*curr-samp-rate] (when-not (= curr-rate frame-rate) (reset! *curr-samp-rate (frame-samp-rate frame)) (refresh-settings))) (let [samples (frame-samples frame) samples-cnt (count samples)] (loop [i 0] (when (< i samples-cnt) (put-sample samples-ring (get samples i)) (recur (long (inc i)))))))) capture-pause-btn (ui/icon-button :icon-name (if @*capturing "mdi-pause" "mdi-record") :tooltip "Start/Stop capturing" :classes ["record-btn"]) _ (ui-utils/set-button-action capture-pause-btn (fn [] (let [c (swap! *capturing not)] (ui-utils/update-button-icon capture-pause-btn (if c "mdi-pause" "mdi-record"))))) controls (ui/h-box :childs [capture-pause-btn (ui/button :label "H-" :on-click (fn [] (swap! *nanos-per-div-idx (fn [i] (min (inc i) (dec (count nanos-per-div))))) (refresh-settings))) (ui/button :label "H+" :on-click (fn [] (swap! *nanos-per-div-idx (fn [i] (max (dec i) 0))) (refresh-settings))) (ui/button :label "V-" :on-click (fn [] (swap! *amp-per-div-idx (fn [i] (min (inc i) (dec (count amp-per-div))))) (refresh-settings))) (ui/button :label "V+" :on-click (fn [] (swap! *amp-per-div-idx (fn [i] (max (dec i) 0))) (refresh-settings))) (ui/button :label "DOWN" :on-click (fn [] (swap! *v-offset + 10))) (ui/button :label "UP" :on-click (fn [] (swap! *v-offset - 10)))] :align :center :spacing 5) settings-labels (ui/h-box :childs [samp-rate-lbl nanos-per-div-lbl units-per-vert-div-lbl capture-samples-lbl] :align :center :spacing 5) box (ui/border-pane :top controls :center canvas :bottom settings-labels)] (add-frame first-frame) (refresh-settings) (.start anim-timer) {:fx/node box :add-frame add-frame :stop-threads (fn [] (.stop anim-timer))})) (defn oscilloscope-update [_ {:keys [add-frame]} {:keys [new-val]}] (add-frame new-val)) (defn oscilloscope-destroy [{:keys [stop-threads]}] (stop-threads)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/data_windows/visualizers.clj ================================================ (ns flow-storm.debugger.ui.data-windows.visualizers (:require [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.ui.utils :as ui-utils] [clojure.string :as str] [flow-storm.debugger.ui.data-windows.visualizers.oscilloscope :as scope]) (:import [javafx.scene.layout Priority HBox VBox])) (defonce *visualizers (atom {})) ;; *defaults-visualizers should be a list (stack) and not a vector so ;; the latest added take precedence and visualizers can be overwritten by users (defonce *defaults-visualizers (atom ())) (defn register-visualizer [{:keys [id] :as viz}] (swap! *visualizers assoc id viz)) (defn visualizers [] @*visualizers) (defn visualizer [viz-id] (get @*visualizers viz-id)) (defn appliable-visualizers [val-data] (->> (vals (visualizers)) (filter (fn [{:keys [pred]}] (pred val-data))))) (defn make-visualizer-for-val [viz-id val] (let [{:keys [on-create]} (visualizer viz-id) viz-ctx (on-create val)] viz-ctx)) (defn add-default-visualizer [pred viz-id] (swap! *defaults-visualizers conj {:pred pred :viz-id viz-id})) (defn default-visualizer [val-data] (let [viz-id (->> @*defaults-visualizers (some (fn [{:keys [pred viz-id]}] (when (pred val-data) viz-id))))] (visualizer viz-id))) (defn data-window-current-val [dw-id] (dbg-state/data-window-current-val dw-id)) ;;;;;;;;;;;;;;;;; ;; Visualizers ;; ;;;;;;;;;;;;;;;;; (register-visualizer {:id :preview :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :previewable)) :on-create (fn [{:keys [preview/pprint]}] {:fx/node (ui/text-area :text pprint :editable? false :class "monospaced")}) :on-update (fn [_ {:keys [fx/node]} {:keys [new-val]}] (ui-utils/set-text-input-text node (str new-val)))}) (register-visualizer {:id :map :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :shallow-map)) :on-create (fn [{:keys [shallow-map/keys-refs shallow-map/vals-refs shallow-map/navs-refs flow-storm.debugger.ui.data-windows.data-windows/dw-id]}] (let [keys-previews (mapv (comp :val-preview meta) keys-refs) vals-previews (mapv (comp :val-preview meta) vals-refs) rows (mapv (fn [k-prev k-ref v-prev v-ref] [{:cell-type :key :k-prev k-prev :k-ref k-ref :stack-key (str k-prev "-KEY")} {:cell-type :val :v-prev v-prev :v-ref v-ref :stack-key k-prev} {:cell-type :nav :nav-ref (get navs-refs k-ref) :stack-key (str k-prev "-NAV")}]) keys-previews keys-refs vals-previews vals-refs)] {:fx/node (:table-view (ui/table-view :columns ["Key" "Val" "Nav"] :cell-factory (fn [_ {:keys [cell-type k-prev k-ref v-prev v-ref nav-ref stack-key]}] (let [extras {:dw-id dw-id :stack-key stack-key}] (case cell-type :key (ui/label :text k-prev :class "link-lbl" :on-click (fn [_] (runtime-api/data-window-push-val-data rt-api dw-id k-ref extras))) :val (ui/label :text v-prev :class "link-lbl" :on-click (fn [_] (runtime-api/data-window-push-val-data rt-api dw-id v-ref extras))) :nav (when nav-ref (ui/button :label ">" :on-click (fn [] (runtime-api/data-window-push-val-data rt-api dw-id nav-ref extras))))))) :columns-width-percs [0.2 0.7 0.1] :resize-policy :constrained :items rows :search-predicate (fn [{:keys [k-prev v-prev]} search-str] (or (str/includes? k-prev search-str) (str/includes? v-prev search-str)))))}))}) (register-visualizer {:id :seqable :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :paged-shallow-seqable)) :on-create (fn [{:keys [flow-storm.debugger.ui.data-windows.data-windows/dw-id] :as seq-page}] (let [{:keys [list-view-pane add-all]} (ui/list-view :editable? false :cell-factory (fn [list-cell {:keys [prev]}] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :class "link-lbl" :text prev)))) :on-click (fn [_ sel-items _] (when-let [{:keys [prev ref]} (first sel-items)] (runtime-api/data-window-push-val-data rt-api dw-id ref {:dw-id dw-id :stack-key prev})))) more-btn (ui/button :label "More") build-and-add-page (fn [{:keys [paged-seq/page-refs paged-seq/next-ref]}] (let [page-previews (mapv (comp :val-preview meta) page-refs)] (add-all (mapv (fn [prev ref] {:prev prev :ref ref}) page-previews page-refs)) (if next-ref (ui-utils/set-button-action more-btn (fn [] (runtime-api/data-window-push-val-data rt-api dw-id next-ref {:update? true}))) (ui-utils/set-disable more-btn true))))] (build-and-add-page seq-page) {:fx/node (ui/v-box :childs [list-view-pane more-btn]) :add-page build-and-add-page})) :on-update (fn [_ {:keys [add-page]} seq-page] (add-page seq-page))}) (register-visualizer {:id :indexed :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :shallow-indexed)) :on-create (fn [{:keys [shallow-idx-coll/vals-refs shallow-idx-coll/navs-refs flow-storm.debugger.ui.data-windows.data-windows/dw-id] :as idx-coll}] (let [vals-previews (mapv (comp :val-preview meta) vals-refs) rows (mapv (fn [idx v-prev v-ref] [{:cell-type :key :idx idx} {:cell-type :val :v-prev v-prev :v-ref v-ref :stack-key (str idx)} {:cell-type :nav :nav-ref (get navs-refs idx) :stack-key (str idx "-NAV")}]) (range) vals-previews vals-refs)] {:fx/node (ui/v-box :childs [(ui/label (format "Count: %d" (:shallow-idx-coll/count idx-coll))) (:table-view (ui/table-view :columns ["Idx" "Val" "Nav"] :columns-width-percs [0.2 0.7 0.1] :resize-policy :constrained :cell-factory (fn [_ {:keys [cell-type idx v-prev v-ref nav-ref stack-key]}] (let [extras {:dw-id dw-id :stack-key stack-key}] (case cell-type :key (ui/label :text (str idx)) :val (ui/label :text v-prev :class "link-lbl" :on-click (fn [_] (runtime-api/data-window-push-val-data rt-api dw-id v-ref extras))) :nav (when nav-ref (ui/button :label ">" :on-click (fn [] (runtime-api/data-window-push-val-data rt-api dw-id nav-ref extras))))))) :search-predicate (fn [{:keys [v-prev]} search-str] (str/includes? v-prev search-str)) :items rows))])}))}) (register-visualizer {:id :int :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :int)) :on-create (fn [#:int {:keys [decimal binary hex octal]}] {:fx/node (ui/v-box :childs [(ui/label :text (format "Decimal: %s" decimal)) (ui/label :text (format "Hex: %s" hex)) (ui/label :text (format "Binary: %s" binary)) (ui/label :text (format "Octal: %s" octal))])})}) (defn- make-bytes-table-pane [data] (let [pad-cells-cnt (- 16 (mod (count data) 16)) right-padded-data (into data (repeat pad-cells-cnt ""))] (:table-view-pane (ui/table-view :columns ["0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15"] :resize-policy :constrained :cell-factory (fn [_ idx] (ui/label :text (get right-padded-data idx))) :items (->> (partition 16 (range (count right-padded-data))) (mapv #(into [] %))))))) (register-visualizer {:id :hex-byte-array :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :byte-array)) :on-create (fn [#:bytes {:keys [full? hex head-hex tail-hex]}] {:fx/node (if full? (make-bytes-table-pane hex) ;; head and tail (ui/v-box :spacing 10 :childs [(ui/label :text "Head") (make-bytes-table-pane head-hex) (ui/label :text "Tail") (make-bytes-table-pane tail-hex)]))})}) (register-visualizer {:id :bin-byte-array :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :byte-array)) :on-create (fn [#:bytes {:keys [full? binary head-binary tail-binary]}] {:fx/node (if full? (make-bytes-table-pane binary) ;; head and tail (ui/v-box :spacing 10 :childs [(ui/label :text "Head") (make-bytes-table-pane head-binary) (ui/label :text "Tail") (make-bytes-table-pane tail-binary)]))})}) (register-visualizer {:id :eql-query-pprint :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :eql-query-pprint)) :on-create (fn [{:keys [eql/pprint eql/query flow-storm.runtime.values/val-ref flow-storm.debugger.ui.data-windows.data-windows/dw-id]}] (let [val-txt (ui/text-area :text pprint :editable? false :class "monospaced") query-txt (ui/text-field :initial-text (pr-str query) :on-return-key (fn [txt] (let [new-query (read-string txt)] (runtime-api/data-window-push-val-data rt-api dw-id val-ref {:update? true :query new-query})))) header-box (ui/h-box :spacing 10 :childs [(ui/label :text "Eql query:") query-txt]) main-pane (ui/border-pane :top header-box :center val-txt)] (HBox/setHgrow query-txt Priority/ALWAYS) (HBox/setHgrow header-box Priority/ALWAYS) (VBox/setVgrow main-pane Priority/ALWAYS) {:fx/node main-pane :redraw (fn [val-pprint] (ui-utils/set-text-input-text val-txt val-pprint))})) :on-update (fn [_ {:keys [redraw]} {:keys [eql/pprint]}] (redraw pprint))}) (register-visualizer {:id :html :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :string)) :on-create (fn [{:keys [string/val]}] (let [{:keys [web-view set-html]} (ui/web-view)] (set-html val) {:fx/node web-view :re-render (fn [s] (set-html s))})) :on-update (fn [_ {:keys [re-render]} {:keys [string/val]}] (re-render val))}) (register-visualizer {:id :oscilloscope :pred (fn [val] (contains? (:flow-storm.runtime.values/kinds val) :oscilloscope-samples-frames)) :on-create scope/oscilloscope-create :on-update scope/oscilloscope-update :on-destroy scope/oscilloscope-destroy}) ;;;;;;;;;;;;;;;;;;;;;;;;; ;; Default visualizers ;; ;;;;;;;;;;;;;;;;;;;;;;;;; ;; The order here matter since they are added on a stack, so the latests ones ;; have preference. (add-default-visualizer (fn [val-data] (= "nil" (:flow-storm.runtime.values/type val-data))) :preview) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :paged-shallow-seqable)) :seqable) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :shallow-indexed)) :indexed) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :shallow-map)) :map) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :number)) :preview) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :int)) :int) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :string)) :preview) (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :oscilloscope-samples-frames)) :oscilloscope) ;; Don't make this the default until we can make its render fast ;; (add-default-visualizer (fn [val-data] (contains? (:flow-storm.runtime.values/kinds val-data) :byte-array)) :hex-byte-array) ================================================ FILE: src-dbg/flow_storm/debugger/ui/docs/screen.clj ================================================ (ns flow-storm.debugger.ui.docs.screen (:require [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.state :refer [store-obj obj-lookup]] [flow-storm.debugger.docs :as dbg-docs] [clojure.string :as str])) (defn create-fn-doc-pane [] (let [fn-name-lbl (ui/label :text "" :class "docs-fn-name") args-box (ui/v-box :childs [] :spacing 10) rets-box (ui/v-box :childs [] :class "docs-box") examples-box (ui/v-box :childs [] :spacing 10) file-lbl (ui/label :text "") line-lbl (ui/label :text "") doc-lbl (ui/label :text "") doc-box (ui/v-box :childs [fn-name-lbl doc-lbl (ui/h-box :childs [(ui/label :text "File :" :class "docs-label") file-lbl]) (ui/h-box :childs [(ui/label :text "Line :" :class "docs-label") line-lbl]) (ui/label :text "Args :" :class "docs-label") args-box (ui/label :text "Returns :" :class "docs-label") rets-box (ui/label :text "Examples :" :class "docs-label") examples-box] :spacing 10) doc-pane (doto (ui/scroll-pane :class "hidden-node") (.setFitToHeight true) (.setFitToWidth true))] (.setContent doc-pane doc-box) (store-obj "docs-fn-name-lbl" fn-name-lbl) (store-obj "docs-args-box" args-box) (store-obj "docs-rets-box" rets-box) (store-obj "docs-examples-box" examples-box) (store-obj "docs-file-lbl" file-lbl) (store-obj "docs-line-lbl" line-lbl) (store-obj "docs-doc-lbl" doc-lbl) (store-obj "docs-doc-pane" doc-pane) doc-pane)) (defn- index-arity [arg-vec] (:arg-vec (reduce (fn [{:keys [i] :as r} symb] (-> r (update :arg-vec conj (cond-> {:symb symb} (not= symb '&) (assoc :i i))) (update :i inc))) {:i 0 :arg-vec []} arg-vec))) (defn- build-type-set-by-idx [args-types] (fn [idx] (let [atypes (reduce (fn [r av-types] (if (< idx (count av-types)) (conj r (or (get av-types idx) "UNKNOWN")) r)) #{} args-types)] (if (> (count atypes) 1) (remove #(= % "UNKNOWN") atypes) atypes)))) (defn- build-map-type-component [{:keys [map/kind map/domain type/name]}] (let [detail-box (case kind :entity (ui/v-box :childs (mapv (fn [[k v]] (ui/label :text (format "%s -> %s" k v))) domain)) :regular (ui/label :text (format "%s -> %s" (-> domain keys first) (-> domain vals first))) (ui/label :text ""))] (ui/v-box :childs [(ui/label :text name :class "docs-type-name") detail-box] :spacing 10))) (defn- coll-literals [type-name] (case type-name "clojure.lang.PersistentVector" ["[" "]"] "clojure.lang.PersistentHashSet" ["#{" "}"] ["(" ")"])) (defn- build-seqable-type-component [{:keys [type/name seq/first-elem-type]}] (let [[open-char close-char] (coll-literals name) f-elem-type-str (cond (nil? first-elem-type) "" (string? first-elem-type) first-elem-type :else (case (:type/type first-elem-type) :fn "Fn" :map (format "{ %s }" (case (:type/kind first-elem-type) :entity (format "< %s >"(->> first-elem-type :domain keys (str/join " "))) :regular (format "%s -> %s" (-> first-elem-type :domain keys first) (-> first-elem-type :domain vals first)) "")) :seqable (:type/name first-elem-type) (:type/name first-elem-type))) details-lbl (ui/label :text (format "%s %s ,...%s" open-char f-elem-type-str close-char))] (ui/h-box :childs [(ui/label :text name :class "docs-type-name") details-lbl] :spacing 10))) (defn build-type-set-component [types-set] (let [build-type-component (fn [t] (case (:type/type t) :fn (ui/label :text "Fn") :map (build-map-type-component t) :seqable (build-seqable-type-component t) ;; else (ui/label :text (:type/name t))))] (ui/v-box :childs (->> types-set (map build-type-component)) :spacing 10))) (defn build-arities-boxes-form-arglists [args-types arglists] (let [arglists (when (seq arglists) (read-string arglists)) type-set-by-idx (build-type-set-by-idx args-types) arglists+ (reduce (fn [r avec] (let [arity-types (->> (index-arity avec) (mapv (fn [{:keys [symb i]}] [symb (when i (type-set-by-idx i))])))] (conj r arity-types))) [] arglists)] (mapv (fn [argv] (let [argv-boxes (mapv (fn [[symb tset]] (let [symb-lbl (doto (ui/label :text (str symb) :class "docs-arg-symbol") (.setMinWidth 70)) types-set-box (build-type-set-component tset)] (ui/h-box :childs [symb-lbl types-set-box] :spacing 10))) argv)] (ui/v-box :childs (-> [(ui/label :text "[")] (into argv-boxes) (into [(ui/label :text "]")])) :class "docs-box" :spacing 10))) arglists+))) (defn build-arities-boxes-from-types [args-types] (let [type-set-by-idx (build-type-set-by-idx args-types) arities-sizes (keys (group-by count args-types))] (map (fn [asize] (ui/v-box :childs (-> [(ui/label :text "[")] (into (mapv (fn [idx] (build-type-set-component (type-set-by-idx idx))) (range asize))) (into [(ui/label :text "]")])) :class "docs-box")) arities-sizes))) (defn update-fn-doc-pane [fn-symb {:keys [args-types return-types call-examples var-meta]}] (let [{:keys [file line arglists doc]} var-meta [fn-name-lbl] (obj-lookup "docs-fn-name-lbl") [args-box] (obj-lookup "docs-args-box") [rets-box] (obj-lookup "docs-rets-box") [examples-box] (obj-lookup "docs-examples-box") [file-lbl] (obj-lookup "docs-file-lbl") [line-lbl] (obj-lookup "docs-line-lbl") [doc-lbl] (obj-lookup "docs-doc-lbl") arities-boxes (if (seq arglists) (build-arities-boxes-form-arglists args-types arglists) (build-arities-boxes-from-types args-types))] (ui-utils/set-text fn-name-lbl (str fn-symb)) (ui-utils/set-text doc-lbl doc) (ui-utils/set-text file-lbl (str file)) (ui-utils/set-text line-lbl (str line)) (ui-utils/observable-clear (.getChildren args-box)) (ui-utils/observable-add-all (.getChildren args-box) arities-boxes) (ui-utils/observable-clear (.getChildren rets-box)) (ui-utils/observable-add-all (.getChildren rets-box) [(build-type-set-component return-types)]) (ui-utils/observable-clear (.getChildren examples-box)) (ui-utils/observable-add-all (.getChildren examples-box) (mapv (fn [{:keys [args ret]}] (ui/v-box :childs [(ui-utils/set-min-size-wrap-content (ui/label :text (format "(%s %s)" (name fn-symb) (str/join " " args)))) (ui/label :text "=>" :class "docs-example-ret-symbol") (ui-utils/set-min-size-wrap-content (ui/label :text (str ret)))] :class "docs-box")) call-examples)))) (defn show-doc [fn-symb] (let [fn-data (get dbg-docs/fn-docs fn-symb) [fn-doc-pane] (obj-lookup "docs-doc-pane")] (ui-utils/rm-class fn-doc-pane "hidden-node") (update-fn-doc-pane fn-symb fn-data))) (defn main-pane [] (let [fn-doc-pane (create-fn-doc-pane) {:keys [list-view-pane add-all]} (ui/list-view :editable? false :cell-factory (fn [list-cell fn-symb] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :text (str fn-symb))))) :on-click (fn [mev sel-items _] (cond (ui-utils/mouse-primary? mev) (show-doc (first sel-items)))) :selection-mode :single :search-predicate (fn [fn-symb search-str] (str/includes? (str fn-symb) search-str))) mp (ui/split :orientation :horizontal :childs [list-view-pane fn-doc-pane] :sizes [0.3])] (add-all (keys dbg-docs/fn-docs)) mp)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/bookmarks.clj ================================================ (ns flow-storm.debugger.ui.flows.bookmarks (:require [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup]] [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui] [clojure.string :as str] [flow-storm.utils :refer [log-error]]) (:import [javafx.scene Scene] [javafx.stage Stage])) (defn update-bookmarks [] (when-let [[{:keys [clear add-all]}] (obj-lookup "bookmarks_table_data")] (let [bookmarks (dbg-state/all-bookmarks)] (clear) (add-all (mapv (fn [b] [(assoc b :cell-type :text, :text (case (:source b) :bookmark.source/ui "ui" :bookmark.source/api "api" )) (assoc b :cell-type :text, :text (str (:flow-id b))) (assoc b :cell-type :text, :text (str (:thread-id b))) (assoc b :cell-type :text, :text (str (:idx b))) (assoc b :cell-type :text, :text (str (:note b))) (assoc b :cell-type :actions)]) bookmarks))))) (defn bookmark-add [flow-id thread-id idx] (let [text (ui/ask-text-dialog :header "Add bookmark" :body "Bookmark name:" :width 800 :height 100 :center-on-stage (dbg-state/main-jfx-stage))] (dbg-state/add-bookmark {:flow-id flow-id :thread-id thread-id :idx idx :note text :source :bookmark.source/ui}) (update-bookmarks))) (defn remove-bookmarks [flow-id] (dbg-state/remove-bookmarks flow-id) (update-bookmarks)) (defn- create-bookmarks-pane [] (let [cell-factory (fn [_ {:keys [cell-type idx text flow-id thread-id]}] (case cell-type :text (ui/label :text text) :actions (ui/icon-button :icon-name "mdi-delete-forever" :on-click (fn [] (dbg-state/remove-bookmark flow-id thread-id idx) (update-bookmarks))))) {:keys [table-view-pane] :as tv-data} (ui/table-view :columns ["Source" "Flow Id" "Thread Id" "Idx" "Note" ""] :columns-width-percs [0.1 0.1 0.1 0.1 0.5 0.1] :cell-factory cell-factory :resize-policy :constrained :on-click (fn [mev sel-items _] (when (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (let [{:keys [flow-id thread-id idx]} (ffirst sel-items) goto-loc (requiring-resolve 'flow-storm.debugger.ui.flows.screen/goto-location)] (goto-loc {:flow-id flow-id :thread-id thread-id :idx idx})))) :selection-mode :multiple :search-predicate (fn [[_ _ _ _ note-column] search-str] (str/includes? (:text note-column) search-str)))] (store-obj "bookmarks_table_data" tv-data) table-view-pane)) (defn show-bookmarks [] (try (let [bookmarks-w 800 bookmarks-h 400 scene (Scene. (create-bookmarks-pane) bookmarks-w bookmarks-h) stage (doto (Stage.) (.setTitle "FlowStorm bookmarks ") (.setScene scene))] (.setOnCloseRequest stage (event-handler [_] (dbg-state/unregister-jfx-stage! stage))) (dbg-state/register-jfx-stage! stage) (let [{:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) bookmarks-w bookmarks-h)] (.setX stage x) (.setY stage y)) (-> stage .show)) (update-bookmarks) (catch Exception e (log-error "UI Thread exception" e)))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/call_tree.clj ================================================ (ns flow-storm.debugger.ui.flows.call-tree (:require [flow-storm.debugger.ui.flows.code :as flow-code] [flow-storm.debugger.ui.flows.general :as ui-flows-gral] [flow-storm.debugger.ui.flows.components :as flow-cmp] [flow-storm.utils :as utils] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup]] [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui]) (:import [javafx.collections ObservableList] [javafx.scene.control TreeCell TreeView TreeItem] [ javafx.beans.value ChangeListener] [javafx.scene.layout HBox Priority VBox])) (def call-stack-tree-childs-limit 500) (defn update-call-stack-tree-pane [flow-id thread-id] (let [lazy-tree-item (fn lazy-tree-item [tree-node] (let [childs-calls (runtime-api/callstack-node-childs rt-api tree-node) limited-calls (->> childs-calls (take call-stack-tree-childs-limit) (remove (fn [child-node] (let [{:keys [fn-name fn-ns]} (runtime-api/callstack-node-frame rt-api child-node)] (dbg-state/callstack-tree-hidden? flow-id thread-id fn-name fn-ns)))) (into [])) final-calls (if (= (count limited-calls) call-stack-tree-childs-limit) (update limited-calls (dec (count limited-calls)) (fn [c] (with-meta c {:truncated-last-child? true}))) limited-calls)] (proxy [TreeItem] [tree-node] (getChildren [] (let [^ObservableList super-childrens (proxy-super getChildren)] (if (.isEmpty super-childrens) (do (.setAll super-childrens ^objects (into-array Object (map lazy-tree-item final-calls))) super-childrens) super-childrens))) (isLeaf [] (empty? final-calls))))) tree-root-node (runtime-api/callstack-tree-root-node rt-api flow-id thread-id) ^TreeItem root-item (lazy-tree-item tree-root-node) [tree-view] (obj-lookup flow-id thread-id "callstack_tree_view")] (.setExpanded root-item true) (.setRoot ^TreeView tree-view root-item))) (defn format-tree-fn-call-args [args-vec] (let [v-str (-> args-vec meta :val-preview ui-utils/remove-newlines) ^String step-1 (utils/elide-string v-str 80)] (if (= \. (.charAt step-1 (dec (count step-1)))) (subs step-1 1 (count step-1)) (subs step-1 1 (dec (count step-1)))))) (defn- create-call-stack-tree-graphic-node [{:keys [form-id fn-name fn-ns args-vec truncated-last-child?] :as frame} item-level] ;; Important ! ;; this will be called for all visible tree nodes after any expansion ;; so it should be fast (if (:root? frame) (ui/label :text ".") (let [{:keys [multimethod/dispatch-val form/form]} (runtime-api/get-form rt-api form-id) form-hint (if (= item-level 1) (utils/elide-string (pr-str form) 80) "")] (ui/h-box :childs (cond-> [(ui/label :text (if dispatch-val (format "(%s/%s %s %s) " fn-ns fn-name (-> dispatch-val meta :val-preview) (format-tree-fn-call-args args-vec)) (format "(%s/%s %s)" fn-ns fn-name (format-tree-fn-call-args args-vec)))) (ui/label :text form-hint :class "light")] truncated-last-child? (conj (ui/label :text "Childs limit reached, truncated ..."))))))) (defn- select-call-stack-tree-node [flow-id thread-id match-idx] (let [[^TreeView tree-view] (obj-lookup flow-id thread-id "callstack_tree_view") [^TreeCell tree-cell] (obj-lookup flow-id thread-id (ui-utils/thread-callstack-tree-cell match-idx))] (when tree-cell (let [tree-cell-idx (.getIndex tree-cell) tree-item (.getTreeItem tree-cell) tree-selection-model (.getSelectionModel tree-view)] (.scrollTo tree-view tree-cell-idx) (ui-utils/selection-select-obj tree-selection-model tree-item))))) (defn expand-path [^TreeItem tree-item select-idx path-set] (when-not (.isEmpty (.getChildren tree-item)) (doseq [^TreeItem child-item (.getChildren tree-item)] (let [cnode (.getValue child-item) {:keys [fn-call-idx]} (runtime-api/callstack-node-frame rt-api cnode)] (when (path-set fn-call-idx) (.setExpanded tree-item true) (when-not (= select-idx fn-call-idx) (expand-path child-item select-idx path-set))))))) (defn expand-and-highlight [flow-id thread-id [curr-idx :as fn-call-idx-path]] (ui-utils/run-later (let [[^TreeView tree-view] (obj-lookup flow-id thread-id "callstack_tree_view") root-item (.getRoot tree-view)] (expand-path root-item curr-idx (into #{} fn-call-idx-path)) (select-call-stack-tree-node flow-id thread-id curr-idx)))) (defn highlight-current-frame [flow-id thread-id] (let [curr-idx (:fn-call-idx (dbg-state/current-timeline-entry flow-id thread-id)) {:keys [fn-call-idx-path]} (runtime-api/frame-data rt-api flow-id thread-id curr-idx {:include-path? true})] (expand-and-highlight flow-id thread-id fn-call-idx-path))) (defn- build-tree-cell-factory [flow-id thread-id ^TreeView tree-view] (proxy [javafx.util.Callback] [] (call [tv] (proxy [TreeCell] [] (updateItem [tree-node empty?] (proxy-super updateItem tree-node empty?) (if empty? (-> this (ui-utils/set-graphic nil) (ui-utils/set-text nil)) (let [^TreeItem tree-item (.getTreeItem ^TreeCell this) item-level (.getTreeItemLevel tree-view tree-item) {:keys [truncated-last-child?]} (meta tree-node) frame (cond-> (runtime-api/callstack-node-frame rt-api tree-node) truncated-last-child? (assoc :truncated-last-child? true)) fn-call-idx (:fn-call-idx frame)] ;; it's the root dummy node, put controls (if (:root? frame) (-> this (ui-utils/set-graphic (ui/icon-button :icon-name "mdi-adjust" :on-click (fn [] (highlight-current-frame flow-id thread-id)) :tooltip "Highlight current frame")) (ui-utils/set-text nil)) ;; else, put the frame (-> this (ui-utils/set-graphic (create-call-stack-tree-graphic-node frame item-level)) (ui-utils/set-text nil))) (store-obj flow-id thread-id (ui-utils/thread-callstack-tree-cell fn-call-idx) this)))))))) (defn create-call-stack-tree-pane [flow-id thread-id] (let [^TreeView tree-view (ui/tree-view) _ (.setCellFactory tree-view (build-tree-cell-factory flow-id thread-id tree-view)) tree-view-sel-model (.getSelectionModel tree-view) get-selected-frame (fn [] (let [sel-tree-node (.getValue ^TreeItem (first (.getSelectedItems tree-view-sel-model)))] (runtime-api/callstack-node-frame rt-api sel-tree-node))) jump-to-selected-frame-code (fn [& _] (let [{:keys [fn-call-idx]} (get-selected-frame)] (ui-flows-gral/select-thread-tool-tab flow-id thread-id "flows-code-stepper") (flow-code/jump-to-coord flow-id thread-id (runtime-api/timeline-entry rt-api flow-id thread-id fn-call-idx :at)))) copy-selected-frame-to-clipboard (fn [args?] (let [{:keys [fn-name fn-ns args-vec]} (get-selected-frame)] (ui-utils/copy-selected-frame-to-clipboard fn-ns fn-name (when args? args-vec)))) _ (doto tree-view (.setOnMouseClicked (event-handler [mev] (when (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (jump-to-selected-frame-code) (ui-utils/consume mev))))) ctx-menu-options [{:text "Step code" :on-click jump-to-selected-frame-code} {:text "Copy qualified function symbol" :on-click (fn [] (copy-selected-frame-to-clipboard false))} {:text "Copy function calling form" :on-click (fn [] (copy-selected-frame-to-clipboard true))} {:text "Hide from tree" :on-click (fn [& _] (let [{:keys [fn-name fn-ns]} (get-selected-frame)] (dbg-state/callstack-tree-hide-fn flow-id thread-id fn-name fn-ns) (update-call-stack-tree-pane flow-id thread-id)))}] ctx-menu (ui/context-menu :items ctx-menu-options) callstack-fn-args-pane (flow-cmp/create-pprint-pane flow-id thread-id "fn_args") callstack-fn-ret-pane (flow-cmp/create-pprint-pane flow-id thread-id "fn_ret") labeled-args-pane (ui/v-box :childs [(ui/label :text "Args:") callstack-fn-args-pane]) labeled-ret-pane (ui/v-box :childs [(ui/label :text "Ret:") callstack-fn-ret-pane]) args-ret-pane (ui/h-box :childs [labeled-args-pane labeled-ret-pane] :spacing 5) top-bottom-split (ui/split :orientation :vertical :childs [tree-view args-ret-pane] :sizes [0.75])] (.setContextMenu tree-view ctx-menu) (VBox/setVgrow callstack-fn-args-pane Priority/ALWAYS) (VBox/setVgrow callstack-fn-ret-pane Priority/ALWAYS) (HBox/setHgrow labeled-args-pane Priority/ALWAYS) (HBox/setHgrow labeled-ret-pane Priority/ALWAYS) (.addListener (.selectedItemProperty tree-view-sel-model) (proxy [ChangeListener] [] (changed [changed old-val ^TreeItem new-val] (when new-val (let [{:keys [args-vec return/kind] :as frame} (runtime-api/callstack-node-frame rt-api (.getValue new-val))] (flow-cmp/update-pprint-pane flow-id thread-id "fn_args" {:val-ref args-vec} {:find-and-jump-same-val (partial flow-code/find-and-jump-same-val flow-id thread-id)}) (flow-cmp/update-pprint-pane flow-id thread-id "fn_ret" {:val-ref (if (= :unwind kind) (:throwable frame) (:ret frame)) :extra-text (case kind :waiting "Return waiting" :unwind "Throwed" nil) :class (case kind :waiting :warning :unwind :fail #_else :normal)} {:find-and-jump-same-val (partial flow-code/find-and-jump-same-val flow-id thread-id)})))))) (store-obj flow-id thread-id "callstack_tree_view" tree-view) (VBox/setVgrow tree-view Priority/ALWAYS) (VBox/setVgrow top-bottom-split Priority/ALWAYS) (when (:call-tree-update? (dbg-state/debugger-config)) (update-call-stack-tree-pane flow-id thread-id)) top-bottom-split)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/code.clj ================================================ (ns flow-storm.debugger.ui.flows.code (:require [clojure.pprint :as pp] [clojure.string :as str] [flow-storm.form-pprinter :as form-pprinter] [flow-storm.debugger.ui.flows.general :refer [open-form-in-editor]] [flow-storm.debugger.ui.commons :refer [def-val]] [flow-storm.debugger.ui.flows.components :as flow-cmp] [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui] [flow-storm.utils :as utils] [flow-storm.debugger.ui.flows.bookmarks :as bookmarks] [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup]] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.tasks :as tasks] [hansel.utils :refer [get-form-at-coord]] [flow-storm.debugger.ui.flows.printer :as printer] [flow-storm.debugger.ui.data-windows.data-windows :as data-windows]) (:import [javafx.scene Node] [javafx.scene.layout Priority VBox HBox] [javafx.scene.control ScrollPane Label SelectionModel] [javafx.scene.text Font] [javafx.scene.input KeyEvent ScrollEvent MouseEvent] [org.fxmisc.richtext CodeArea] [org.fxmisc.richtext.model StyleSpansBuilder TwoDimensional$Bias])) (declare jump-to-coord) (declare find-and-jump-same-val) (defn- maybe-unwrap-runi-tokens "Unwrap from (fn* flowstorm-runi ([] )) if it is wrapped, returns print-tokens otherwise. Works at print-tokens level instead of at form level." [print-tokens] ;; This is as hacky as it gets but it is also for the vanilla non fn expressions ;; typed at the repl, like (+ 1 2) (if-let [runi-token-idx (some (fn [[i {:keys [text]}]] (when (= text "flowstorm-runi") i)) (map vector (range) (take 10 print-tokens)))] (let [expands-into-multiple-lines? (= runi-token-idx 5) wrap-beg (if expands-into-multiple-lines? 13 9) wrap-end (- (count print-tokens) 2) sub-tokens (subvec print-tokens wrap-beg wrap-end) expr-offset (+ (count "(fn* flowstorm-runi ([] ") (if expands-into-multiple-lines? 4 0))] (->> sub-tokens ;; since we removed some tokens we need to move all idx-from back (map (fn [{:keys [idx-from] :as tok}] (if idx-from (update tok :idx-from #(- % expr-offset)) tok))))) print-tokens)) (defn- jump-to-record-here [flow-id thread-id form-id coord {:keys [backward? from-idx]}] (tasks/submit-task runtime-api/find-expr-entry-task [{:flow-id flow-id :thread-id thread-id :from-idx from-idx :backward? backward? :coord coord :form-id form-id}] {:on-finished (fn [{:keys [result]}] (when result (jump-to-coord flow-id thread-id result)))})) (defn- add-to-printer "Presents the user a dialog asking for a message and adds a print to the printer tool at the timeline-entry coord." [flow-id thread-id {:keys [coord fn-call-idx]}] (let [{:keys [form-id fn-ns fn-name]} (runtime-api/timeline-entry rt-api flow-id thread-id fn-call-idx :at) form (runtime-api/get-form rt-api form-id) source-expr (get-form-at-coord (:form/form form) coord) params-map (ui/ask-text-dialog+ :header "Printer message. Check textfields tooltips for help." :bodies [{:key :message-format :label "Message format" :tooltip "Use %s if you want to reposition the val in the message."} {:key :expr-str :label "Expression" :tooltip "Use functions like #(str (:name %) (:age %)) to transform the value. (No ClojureScript support yet)"}] :width 800 :height 100 :center-on-stage (dbg-state/main-jfx-stage))] (dbg-state/add-printer flow-id form-id coord (assoc (dissoc form :form/form) :fn-ns fn-ns :fn-name fn-name :coord coord :source-expr source-expr :format-str (:message-format params-map) :transform-expr-str (:expr-str params-map) :print-length 5 :print-level 3 :enable? true)) (printer/update-prints-controls flow-id))) (defn- calculate-execution-idx-range [spans curr-coord] (when curr-coord (let [[s1 s2 :as hl-coords-spans] (->> spans (map-indexed (fn [i s] (assoc s :i i))) (filter (fn [{:keys [coord]}] (= coord curr-coord))))] (case (count hl-coords-spans) 1 [(:idx-from s1) (+ (:idx-from s1) (:len s1))] 2 [(:idx-from s1) (+ (:idx-from s2) (:len s2))] nil)))) (defn- build-style-spans "Given coord-spans as generated by `form-pprinter/coord-spans` build StyleSpans to be used in RichTextFX CodeAreas" [coord-spans curr-coord] (let [^StyleSpansBuilder spb (StyleSpansBuilder.) [exec-from exec-to] (calculate-execution-idx-range coord-spans curr-coord)] (doseq [{:keys [idx-from len coord interesting? tab?]} coord-spans] (let [executing? (and exec-from exec-to (<= exec-from idx-from (+ idx-from len) exec-to)) color-classes (cond-> ["code-token"] (and coord (not interesting?)) (conj "possible") (and executing? (not tab?)) (conj "executing") (and executing? tab?) (conj "executing-dim") interesting? (conj "interesting"))] (.add spb color-classes len))) (.create spb))) (defn- make-coord-expressions-menu [flow-id thread-id clicked-coord-exprs curr-idx] (let [first-entry (first clicked-coord-exprs) last-entry (get clicked-coord-exprs (dec (count clicked-coord-exprs))) interesting-N 20 interesting-entries (cond ;; if we are currently before this expressions ;; show the first N discarding the first one since we ;; add it as a special entry (< curr-idx (:idx first-entry)) (subvec clicked-coord-exprs 1 (min interesting-N (count clicked-coord-exprs))) ;; if we are currently after this expressions ;; show the last N discarding the last one since we ;; add it as a special entry (> curr-idx (:idx last-entry)) (subvec clicked-coord-exprs (max 0 (- (count clicked-coord-exprs) interesting-N)) (- (count clicked-coord-exprs) 2)) ;; if we are somewhere in the middle show N/2 before ;; and N/2 after our current position :else (let [{:keys [before match after]} (utils/grep-coll clicked-coord-exprs (/ interesting-N 2) (/ interesting-N 2) (fn [{:keys [idx]}] (> idx curr-idx)))] (-> before (into [{:idx curr-idx}]) (into [match]) (into after)))) make-menu-item (fn [{:keys [idx result]}] (if (= idx curr-idx) {:text (format "%d - << we are here >>" curr-idx) :disable? true} (let [v-str (-> result meta :val-preview (utils/elide-string 80))] {:text (cond (= (:idx first-entry) idx) (format "[FIRST] %d - %s" idx v-str) (= (:idx last-entry) idx) (format "[LAST] %d - %s" idx v-str) :else (format "%d - %s" idx v-str)) :on-click #(jump-to-coord flow-id thread-id (runtime-api/timeline-entry rt-api flow-id thread-id idx :at))}))) ctx-menu-options (mapv make-menu-item (-> [first-entry] (into interesting-entries) (into [last-entry]) ;; kind of hack. On small cases when we are in the middle ;; but close to the border we are going to have first and last ;; already added by interesting-entries dedupe)) loop-traces-menu (ui/context-menu :items ctx-menu-options)] loop-traces-menu)) (defn copy-current-frame-symbol [flow-id thread-id args?] (let [{:keys [fn-name fn-ns args-vec]} (dbg-state/current-frame flow-id thread-id)] (ui-utils/copy-selected-frame-to-clipboard fn-ns fn-name (when args? args-vec)))) (defn- build-form-paint-and-arm-fn "Builds a form-paint-fn function that when called with expr-executions and a curr-coord will repaint and arm the form-code-area with the interesting and currently executing tokens. All interesting tokens will be clickable." [flow-id thread-id form ^CodeArea form-code-area print-tokens] (let [[thread-scroll-pane] (obj-lookup flow-id thread-id "forms_scroll")] (fn [expr-executions curr-coord] (let [interesting-coords (group-by :coord expr-executions) spans (->> print-tokens (map (fn [{:keys [coord] :as tok}] (if (contains? interesting-coords coord) (assoc tok :interesting? true) tok))) form-pprinter/coord-spans) exec-idx (some (fn [{:keys [coord idx-from]}] (when (= coord curr-coord) idx-from)) spans) style-spans (build-style-spans spans curr-coord)] (when exec-idx (.moveTo form-code-area exec-idx) (.requestFollowCaret form-code-area) (let [caret-pos (.getCaretPosition form-code-area) caret-pos-2d (.offsetToPosition form-code-area caret-pos TwoDimensional$Bias/Forward) caret-line (.getMajor caret-pos-2d) area-lines (-> form-code-area .getParagraphs .size) caret-area-perc (if (pos? area-lines) (float (/ caret-line area-lines)) 0)] (ui-utils/ensure-node-visible-in-scroll-pane thread-scroll-pane form-code-area caret-area-perc))) (.setStyleSpans form-code-area 0 0 style-spans) (.setOnMouseClicked form-code-area (event-handler [^MouseEvent mev] (let [char-hit (.hit form-code-area (.getX mev) (.getY mev)) opt-char-idx (.getCharacterIndex char-hit) ctx-menu-options (cond-> [{:text "Copy qualified function symbol" :on-click (fn [] (copy-current-frame-symbol flow-id thread-id false))} {:text "Copy function calling form" :on-click (fn [] (copy-current-frame-symbol flow-id thread-id true))}] (not (dbg-state/clojure-storm-env?)) (into [{:text "Fully instrument this form" :on-click (fn [] (runtime-api/eval-form rt-api (pr-str (:form/form form)) {:instrument? true :ns (:form/ns form)}))} {:text "Instrument this form without bindings" :on-click (fn [] (runtime-api/eval-form rt-api (pr-str (:form/form form)) {:instrument? true :instrument-options {:disable #{:bind}} :ns (:form/ns form)}))}]))] (if (.isPresent opt-char-idx) (let [char-idx (.getAsInt opt-char-idx) clicked-span (->> spans (some (fn [{:keys [idx-from len] :as span}] (when (and (>= char-idx idx-from) (< char-idx (+ idx-from len))) span)))) ctx-menu-options (cond-> ctx-menu-options (:line clicked-span) (into [{:text "Open in editor" :on-click (fn [] (open-form-in-editor form (:line clicked-span)))}])) curr-idx (dbg-state/current-idx flow-id thread-id)] (when-let [coord (:coord clicked-span)] (if (:interesting? clicked-span) (let [clicked-coord-exprs (get interesting-coords coord)] (if (ui-utils/mouse-secondary? mev) (ui-utils/show-context-menu :menu (ui/context-menu :items (into ctx-menu-options [{:text "Add to prints" :on-click #(add-to-printer flow-id thread-id (first clicked-coord-exprs))}])) :parent form-code-area :mouse-ev mev) ;; else (if (= 1 (count clicked-coord-exprs)) (jump-to-coord flow-id thread-id (first clicked-coord-exprs)) (ui-utils/show-context-menu :menu (make-coord-expressions-menu flow-id thread-id clicked-coord-exprs curr-idx) :parent form-code-area :mouse-ev mev)))) ;; else if it is not interesting? we don't want to jump there ;; but provide a way of search and jump to it by coord and form (let [form-id (:form/id form)] (when (ui-utils/mouse-secondary? mev) (ui-utils/show-context-menu :menu (ui/context-menu :items (into [{:text "Jump to first record here" :on-click (fn [] (jump-to-record-here flow-id thread-id form-id coord {:backward? false :from-idx 0}))} {:text "Jump forward here" :on-click (fn [] (jump-to-record-here flow-id thread-id form-id coord {:backward? false :from-idx curr-idx}))} {:text "Jump backwards here" :on-click (fn [] (jump-to-record-here flow-id thread-id form-id coord {:backward? true :from-idx curr-idx}))}] ctx-menu-options)) :parent form-code-area :mouse-ev mev)))))) ;; else clicked on the form background (when (ui-utils/mouse-secondary? mev) (ui-utils/show-context-menu :menu (ui/context-menu :items ctx-menu-options) :parent form-code-area :mouse-ev mev)))))))))) (defn- add-form "Pprints and adds a form to the flow and thread forms_box container." [form flow-id thread-id form-id] (let [print-tokens (binding [pp/*print-right-margin* 80] (-> (form-pprinter/pprint-tokens (:form/form form)) ;; if it is a wrapped repl expression discard some tokens that the user ;; isn't interested in maybe-unwrap-runi-tokens)) [^VBox forms-box] (obj-lookup flow-id thread-id "forms_box") code-text (form-pprinter/to-string print-tokens) ns-label (let [form-line (some-> form :form/form meta :line) ^Label ns-lbl (ui/label :text (if form-line (format "%s:%d" (:form/ns form) form-line) (:form/ns form)) :class "link-lbl-no-color")] (doto ns-lbl (.setOnMouseClicked (event-handler [_] (open-form-in-editor form))) (.setFont (Font. 10)))) form-header (ui/h-box :childs [ns-label] :align :top-right) ^CodeArea form-code-area (ui/code-area :editable? false :text code-text) form-pane (ui/v-box :childs [form-header form-code-area] :class "form-pane") form-paint-fn (build-form-paint-and-arm-fn flow-id thread-id form form-code-area print-tokens)] ;; The code area when focused will capture all keyboard events, so we ;; re-fire them so they can be handled up in the chain (.addEventFilter form-code-area KeyEvent/ANY (event-handler [^KeyEvent kev] (.fireEvent ^Node forms-box (.copyFor kev form-code-area ^Node forms-box)))) (.addEventFilter form-code-area ScrollEvent/ANY (event-handler [^ScrollEvent sev] (.fireEvent ^Node forms-box (.copyFor sev form-code-area ^Node forms-box)))) (ui-utils/add-class form-code-area "form-pane") (store-obj flow-id thread-id (ui-utils/thread-form-box-id form-id) form-pane) (store-obj flow-id thread-id (ui-utils/thread-form-paint-fn form-id) form-paint-fn) (-> forms-box .getChildren (.add 0 form-pane)) form-pane)) (defn- locals-cell-factory [_ {:keys [cell-type symb-name val-ref]}] (case cell-type :symbol (ui/label :text symb-name) :val-ref (ui/label :text (-> val-ref meta :val-preview (utils/elide-string 80))))) (defn- on-locals-item-click [flow-id thread-id mev selected-items {:keys [table-view-pane]}] (when (ui-utils/mouse-secondary? mev) (let [[_ {:keys [val-ref]}] (first selected-items) ctx-menu (ui/context-menu :items [{:text "Define all" :on-click (fn [] (let [curr-idx (dbg-state/current-idx flow-id thread-id) {:keys [fn-ns]} (dbg-state/current-frame flow-id thread-id) all-bindings (runtime-api/bindings rt-api flow-id thread-id curr-idx {})] (doseq [[symb-name vref] all-bindings] (let [symb (symbol fn-ns symb-name)] (runtime-api/def-value rt-api symb vref)))))} {:text "Define var for val" :on-click (fn [] (def-val val-ref))} {:text "Tap val" :on-click (fn [] (runtime-api/tap-value rt-api val-ref))} {:text "Inspect" :on-click (fn [] (data-windows/create-data-window-for-vref val-ref))}])] (ui-utils/show-context-menu :menu ctx-menu :parent table-view-pane :mouse-ev mev)))) (defn- create-locals-pane [flow-id thread-id] (let [{:keys [table-view-pane] :as tv-data} (ui/table-view :columns ["Binding" "Value"] :cell-factory locals-cell-factory :resize-policy :constrained :on-click (partial on-locals-item-click flow-id thread-id) :selection-mode :single :search-predicate (fn [[{:keys [symb-name]} _] search-str] (str/includes? symb-name search-str)))] (store-obj flow-id thread-id "locals_table" tv-data) table-view-pane)) (defn- update-locals-pane [flow-id thread-id bindings] (let [[{:keys [clear add-all]}] (obj-lookup flow-id thread-id "locals_table")] (clear) (->> bindings (sort-by first) (mapv (fn [[symb-name val-ref]] [{:cell-type :symbol :symb-name symb-name} {:cell-type :val-ref :val-ref val-ref}])) add-all))) (defn- create-stack-pane [flow-id thread-id] (let [cell-factory (fn [list-cell {:keys [fn-ns fn-name form-def-kind dispatch-val]}] (ui-utils/set-graphic list-cell (ui/label :text (if (= :defmethod form-def-kind) (str fn-ns "/" fn-name " " dispatch-val) (str fn-ns "/" fn-name)) :class "link-lbl"))) item-click (fn [mev selected-items _] (let [{:keys [fn-call-idx]} (first selected-items)] (when (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (jump-to-coord flow-id thread-id (runtime-api/timeline-entry rt-api flow-id thread-id fn-call-idx :at))))) {:keys [list-view-pane] :as lv-data} (ui/list-view :editable? false :selection-mode :single :cell-factory cell-factory :on-click item-click)] (store-obj flow-id thread-id "stack_list" lv-data) list-view-pane)) (defn- update-frames-stack [flow-id thread-id fn-call-idx] (let [stack (runtime-api/stack-for-frame rt-api flow-id thread-id fn-call-idx) [{:keys [clear add-all]}] (obj-lookup flow-id thread-id "stack_list")] (clear) (add-all stack))) (defn update-thread-trace-count-lbl [flow-id thread-id] (when-let [[lbl] (obj-lookup flow-id thread-id "thread_trace_count_lbl")] (let [cnt (runtime-api/timeline-count rt-api flow-id thread-id)] (ui-utils/set-text lbl (str (dec cnt)))))) (defn- unhighlight-form [flow-id thread-id form-id] (let [[form-pane] (obj-lookup flow-id thread-id (ui-utils/thread-form-box-id form-id))] (when form-pane (ui-utils/rm-class form-pane "form-background-highlighted")))) (defn add-or-highlight-form [flow-id thread-id form-id] (let [form (runtime-api/get-form rt-api form-id) [form-pane] (obj-lookup flow-id thread-id (ui-utils/thread-form-box-id form-id)) ;; if the form we are about to highlight doesn't exist in the view add it first form-pane (or form-pane (add-form form flow-id thread-id form-id))] (ui-utils/add-class form-pane "form-background-highlighted"))) (defn un-highlight-form-tokens [flow-id thread-id form-id] (let [[form-paint-fn] (obj-lookup flow-id thread-id (ui-utils/thread-form-paint-fn form-id))] (when form-paint-fn (form-paint-fn [] nil)))) (defn highlight-interesting-form-tokens [flow-id thread-id form-id frame-data entry] (let [{:keys [expr-executions]} frame-data [form-paint-fn] (obj-lookup flow-id thread-id (ui-utils/thread-form-paint-fn form-id))] (when form-paint-fn (form-paint-fn expr-executions (:coord entry))))) (defn- make-output-datawindow-id [flow-id thread-id] (keyword (format "expr-result-flow-%s-thread-%s" flow-id thread-id))) (defn jump-to-coord [flow-id thread-id next-tentry] (try (when (:debug-mode? (dbg-state/debugger-config)) (utils/log (str "Jump to " next-tentry))) (let [curr-frame (if-let [cfr (dbg-state/current-frame flow-id thread-id)] cfr ;; if we don't have a current frame it means it is the first ;; jump so, initialize the debugger thread state (let [first-frame (runtime-api/frame-data rt-api flow-id thread-id 0 {:include-exprs? true}) first-tentry (runtime-api/timeline-entry rt-api flow-id thread-id 0 :at)] (dbg-state/set-current-frame flow-id thread-id first-frame) (dbg-state/set-current-timeline-entry flow-id thread-id first-tentry) first-frame)) curr-tentry (dbg-state/current-timeline-entry flow-id thread-id) curr-idx (:idx curr-tentry) next-idx (:idx next-tentry) curr-fn-call-idx (:fn-call-idx curr-frame) next-fn-call-idx (:fn-call-idx next-tentry) changing-frame? (not= curr-fn-call-idx next-fn-call-idx) next-frame (if changing-frame? (let [nfr (runtime-api/frame-data rt-api flow-id thread-id next-fn-call-idx {:include-exprs? true})] nfr) curr-frame) curr-form-id (:form-id curr-frame) next-form-id (:form-id next-frame) [curr-trace-text-field] (obj-lookup flow-id thread-id "thread_curr_trace_tf") first-jump? (and (zero? curr-idx) (zero? next-idx)) changing-form? (not= curr-form-id next-form-id)] ;; update thread current trace label and total traces (ui-utils/set-text-input-text curr-trace-text-field (str next-idx)) (update-thread-trace-count-lbl flow-id thread-id) (when (or first-jump? changing-frame?) (un-highlight-form-tokens flow-id thread-id curr-form-id) (update-frames-stack flow-id thread-id next-fn-call-idx) (when (or first-jump? changing-form?) (unhighlight-form flow-id thread-id curr-form-id) (add-or-highlight-form flow-id thread-id next-form-id))) (highlight-interesting-form-tokens flow-id thread-id next-form-id next-frame next-tentry) ;; update expression reusult panels (let [[val-ref e-text class] (case (:type next-tentry) :fn-call [nil "Call" :normal] :expr [(:result next-tentry) "" :normal] :fn-return [(:result next-tentry) "Return" :normal] :fn-unwind [(:throwable next-tentry) "Throws" :fail]) dw-id (make-output-datawindow-id flow-id thread-id)] (flow-cmp/update-pprint-pane flow-id thread-id "expr_result" {:val-ref val-ref :extra-text e-text :class class} {:find-and-jump-same-val (partial find-and-jump-same-val flow-id thread-id)}) (runtime-api/data-window-push-val-data rt-api dw-id val-ref {:root? true :dw-id dw-id :stack-key "expr-result" :preferred-size :small})) ;; update locals panel (update-locals-pane flow-id thread-id (runtime-api/bindings rt-api flow-id thread-id next-idx {})) (when changing-frame? (dbg-state/set-current-frame flow-id thread-id next-frame)) (dbg-state/set-current-timeline-entry flow-id thread-id next-tentry) (dbg-state/update-nav-history flow-id thread-id next-tentry)) (catch Throwable e (utils/log-error (str "Error jumping into " flow-id " " thread-id " " next-tentry) e)))) (defn step-prev [flow-id thread-id] (let [curr-idx (dbg-state/current-idx flow-id thread-id) prev-tentry (runtime-api/timeline-entry rt-api flow-id thread-id curr-idx :prev)] (jump-to-coord flow-id thread-id prev-tentry))) (defn step-next [flow-id thread-id] (let [curr-idx (dbg-state/current-idx flow-id thread-id) next-tentry (runtime-api/timeline-entry rt-api flow-id thread-id curr-idx :next)] (jump-to-coord flow-id thread-id next-tentry))) (defn step-next-over [flow-id thread-id] (let [curr-idx (dbg-state/current-idx flow-id thread-id) next-tentry (runtime-api/timeline-entry rt-api flow-id thread-id curr-idx :next-over)] (jump-to-coord flow-id thread-id next-tentry))) (defn step-prev-over [flow-id thread-id] (let [curr-idx (dbg-state/current-idx flow-id thread-id) prev-tentry (runtime-api/timeline-entry rt-api flow-id thread-id curr-idx :prev-over)] (jump-to-coord flow-id thread-id prev-tentry))) (defn step-out [flow-id thread-id] (let [curr-idx (dbg-state/current-idx flow-id thread-id) out-tentry (runtime-api/timeline-entry rt-api flow-id thread-id curr-idx :next-out)] (jump-to-coord flow-id thread-id out-tentry))) (defn step-first [flow-id thread-id] (let [first-tentry (runtime-api/timeline-entry rt-api flow-id thread-id 0 :at)] (jump-to-coord flow-id thread-id first-tentry))) (defn step-last [flow-id thread-id] (let [last-idx (dec (runtime-api/timeline-count rt-api flow-id thread-id)) last-tentry (runtime-api/timeline-entry rt-api flow-id thread-id last-idx :at)] (jump-to-coord flow-id thread-id last-tentry))) (defn find-and-jump [current-flow-id current-thread-id search-params] (tasks/submit-task runtime-api/find-expr-entry-task [search-params] {:on-finished (fn [{:keys [result]}] (when result (let [{:keys [thread-id idx] :as next-tentry} result] (if (= current-thread-id thread-id) (jump-to-coord current-flow-id current-thread-id next-tentry) (let [goto-loc (requiring-resolve 'flow-storm.debugger.ui.flows.screen/goto-location)] (goto-loc {:flow-id current-flow-id :thread-id thread-id :idx idx}))))))})) (defn find-and-jump-same-val [flow-id thread-id v-ref backward?] (let [{:keys [idx]} (dbg-state/current-timeline-entry flow-id thread-id) from-idx (if backward? (dec idx) (inc idx))] (find-and-jump flow-id thread-id {:identity-val v-ref :flow-id flow-id :thread-id thread-id :backward? backward? :from-idx from-idx}))) (defn- power-stepping-pane [flow-id thread-id] (let [custom-expression-txt (ui/text-field :initial-text "(fn [v] v)") *selected-fn (atom nil) fn-selector (ui/autocomplete-textfield :on-select-set-text? true :get-completions (fn [] (into [] (keep (fn [{:keys [fn-ns fn-name]}] (when-not (re-find #"fn--[\d]+$" fn-name) {:text (format "%s/%s" fn-ns fn-name) :on-select (fn [] (reset! *selected-fn (symbol fn-ns fn-name)))}))) (runtime-api/fn-call-stats rt-api flow-id thread-id)))) show-custom-field (fn [field] (doto custom-expression-txt (.setVisible false) (.setPrefWidth 0)) (doto fn-selector (.setVisible false) (.setPrefWidth 0)) (case field :custom-txt (doto custom-expression-txt (.setVisible true) (.setPrefWidth 200)) :fn-selector (doto fn-selector (.setVisible true) (.setPrefWidth 200)) nil)) _ (show-custom-field nil) step-type-combo (ui/combo-box :items ["identity" "identity-other-thread" "equality" "same-coord" "custom" "custom-same-coord" "fn-call"] :on-change (fn [_ new-val] (case new-val "identity" (show-custom-field nil) "identity-other-thread" (show-custom-field nil) "equality" (show-custom-field nil) "same-coord" (show-custom-field nil) "custom" (show-custom-field :custom-txt) "custom-same-coord" (show-custom-field :custom-txt) "fn-call" (show-custom-field :fn-selector)))) search-params (fn [backward?] (let [^SelectionModel sel-model (.getSelectionModel step-type-combo) step-type-val (.getSelectedItem sel-model) tentry (dbg-state/current-timeline-entry flow-id thread-id) [idx target-val coord] (if (= :fn-unwind (:type tentry)) [(:idx tentry) (:throwable tentry) (:coord tentry)] [(:idx tentry) (:result tentry) (:coord tentry)]) {:keys [form-id]} (dbg-state/current-frame flow-id thread-id) from-idx (if backward? (dec idx) (inc idx)) sel-fn-call-symb @*selected-fn params (case step-type-val "identity" {:identity-val target-val :thread-id thread-id :backward? backward? :from-idx from-idx} "identity-other-thread" {:identity-val target-val :from-idx 0 :skip-threads #{thread-id} :backward? false} "equality" {:equality-val target-val :thread-id thread-id :backward? backward? :from-idx from-idx} "same-coord" {:coord coord :form-id form-id :thread-id thread-id :backward? backward? :from-idx from-idx} "custom" {:custom-pred-form (.getText custom-expression-txt) :thread-id thread-id :backward? backward? :from-idx from-idx} "custom-same-coord" {:custom-pred-form (.getText custom-expression-txt) :coord coord :form-id form-id :thread-id thread-id :backward? backward? :from-idx from-idx} "fn-call" {:fn-ns (namespace sel-fn-call-symb) :fn-name (name sel-fn-call-symb) :from-idx from-idx :thread-id thread-id :backward? backward?})] (assoc params :flow-id flow-id))) val-first-btn (ui/icon-button :icon-name "mdi-ray-start" :on-click (fn [] (find-and-jump flow-id thread-id (-> (search-params false) (assoc :from-idx 0)))) :tooltip "Power step to the first expression") val-prev-btn (ui/icon-button :icon-name "mdi-ray-end-arrow" :on-click (fn [] (find-and-jump flow-id thread-id (search-params true))) :tooltip "Power step to the prev expression") val-next-btn (ui/icon-button :icon-name "mdi-ray-start-arrow" :on-click (fn [] (find-and-jump flow-id thread-id (search-params false))) :tooltip "Power step to the next expression") val-last-btn (ui/icon-button :icon-name "mdi-ray-end" :on-click (fn [] (find-and-jump flow-id thread-id (-> (search-params true) (dissoc :from-idx)))) :tooltip "Power step to the last expression") power-stepping-pane (ui/h-box :childs [val-first-btn val-prev-btn val-next-btn val-last-btn step-type-combo custom-expression-txt fn-selector] :spacing 3)] power-stepping-pane)) (defn undo-jump [flow-id thread-id] (binding [dbg-state/*undo-redo-jump* true] (jump-to-coord flow-id thread-id (dbg-state/undo-nav-history flow-id thread-id)))) (defn redo-jump [flow-id thread-id] (binding [dbg-state/*undo-redo-jump* true] (jump-to-coord flow-id thread-id (dbg-state/redo-nav-history flow-id thread-id)))) (defn- trace-pos-pane [flow-id thread-id] (let [first-btn (ui/icon-button :icon-name "mdi-page-first" :on-click (fn [] (step-first flow-id thread-id)) :tooltip "Step to the first recorded expression") last-btn (ui/icon-button :icon-name "mdi-page-last" :on-click (fn [] (step-last flow-id thread-id)) :tooltip "Step to the last recorded expression") curr-trace-text-field (ui/text-field :initial-text "0" :on-return-key (fn [idx-str] (let [[^ScrollPane forms-scroll-pane] (obj-lookup flow-id thread-id "forms_scroll") target-idx (Long/parseLong idx-str) target-tentry (runtime-api/timeline-entry rt-api flow-id thread-id target-idx :at)] (jump-to-coord flow-id thread-id target-tentry) (.requestFocus forms-scroll-pane))) :align :center-right :pref-width 80) separator-lbl (ui/label :text "/") thread-trace-count-lbl (ui/label :text "?")] (store-obj flow-id thread-id "thread_curr_trace_tf" curr-trace-text-field) (store-obj flow-id thread-id "thread_trace_count_lbl" thread-trace-count-lbl) (ui/h-box :childs [first-btn curr-trace-text-field separator-lbl thread-trace-count-lbl last-btn] :class "trace-position-box" :spacing 2))) (defn- create-bookmarks-and-nav-pane [flow-id thread-id] (let [bookmark-btn (ui/icon-button :icon-name "mdi-bookmark" :on-click (fn [] (bookmarks/bookmark-add flow-id thread-id (dbg-state/current-idx flow-id thread-id))) :tooltip "Bookmark the current position") undo-nav-btn (ui/icon-button :icon-name "mdi-undo" :on-click (fn [] (undo-jump flow-id thread-id)) :tooltip "Undo navigation") redo-nav-btn (ui/icon-button :icon-name "mdi-redo" :on-click (fn [] (redo-jump flow-id thread-id)) :tooltip "Redo navigation")] (ui/h-box :childs [undo-nav-btn redo-nav-btn bookmark-btn] :spacing 2))) (defn- create-controls-first-row-pane [flow-id thread-id] (let [bookmarks-and-nav-pane (create-bookmarks-and-nav-pane flow-id thread-id)] (ui/h-box :childs [bookmarks-and-nav-pane (power-stepping-pane flow-id thread-id)] :class "thread-controls-pane" :spacing 20))) (defn- create-controls-second-row-pane [flow-id thread-id] (let [prev-over-btn (ui/icon-button :icon-name "mdi-debug-step-over" :on-click (fn [] (step-prev-over flow-id thread-id)) :tooltip "Step over to the previous expression in the current frame" :mirrored? true) prev-btn (ui/icon-button :icon-name "mdi-chevron-left" :on-click (fn [] (step-prev flow-id thread-id)) :tooltip "Step in the previous expression") out-btn (ui/icon-button :icon-name "mdi-debug-step-out" :on-click (fn [] (step-out flow-id thread-id)) :tooltip "Step out to the caller, right after calling this funciton.") next-btn (ui/icon-button :icon-name "mdi-chevron-right" :on-click (fn [] (step-next flow-id thread-id)) :tooltip "Step in the next expression") next-over-btn (ui/icon-button :icon-name "mdi-debug-step-over" :on-click (fn [] (step-next-over flow-id thread-id)) :tooltip "Step over to the next expression in the current frame") controls-box (ui/h-box :childs [prev-over-btn prev-btn out-btn next-btn next-over-btn] :spacing 2)] (ui/h-box :childs [controls-box (trace-pos-pane flow-id thread-id)] :class "thread-controls-pane" :spacing 20))) (defn- create-forms-pane [flow-id thread-id] (let [^VBox forms-box (ui/v-box :childs [] :spacing 5) _ (.setOnScroll forms-box (event-handler [^ScrollEvent ev] (when (or (.isAltDown ev) (.isControlDown ev)) (.consume ev) (cond (> (.getDeltaY ev) 0) (step-prev flow-id thread-id) (< (.getDeltaY ev) 0) (step-next flow-id thread-id))))) ^ScrollPane scroll-pane (ui/scroll-pane :class "forms-scroll-container") controls-first-row-pane (create-controls-first-row-pane flow-id thread-id) controls-second-row-pane (create-controls-second-row-pane flow-id thread-id) outer-box (ui/v-box :childs [controls-first-row-pane controls-second-row-pane scroll-pane])] (VBox/setVgrow forms-box Priority/ALWAYS) (VBox/setVgrow scroll-pane Priority/ALWAYS) (HBox/setHgrow scroll-pane Priority/ALWAYS) (-> forms-box .prefWidthProperty (.bind (.widthProperty scroll-pane))) (.setContent scroll-pane forms-box) (store-obj flow-id thread-id "forms_box" forms-box) (store-obj flow-id thread-id "forms_scroll" scroll-pane) outer-box)) (defn- create-result-pane [flow-id thread-id] (let [pprint-tab (ui/tab :graphic (ui/icon :name "mdi-code-braces") :content (flow-cmp/create-pprint-pane flow-id thread-id "expr_result")) dw-id (make-output-datawindow-id flow-id thread-id) dw-tab (ui/tab :graphic (ui/icon :name "mdi-flash-red-eye") :content (data-windows/data-window-pane {:data-window-id dw-id})) tools-tab-pane (ui/tab-pane :closing-policy :unavailable :tabs [dw-tab pprint-tab])] tools-tab-pane)) (defn create-code-pane [flow-id thread-id] (let [forms-pane (create-forms-pane flow-id thread-id) result-pane (create-result-pane flow-id thread-id) locals-stack-tab-pane (ui/tab-pane :tabs [(ui/tab :text "Locals" :content (create-locals-pane flow-id thread-id) :tooltip "Locals") (ui/tab :text "Stack" :content (create-stack-pane flow-id thread-id) :tooltip "Locals")] :side :top :closing-policy :unavailable) locals-result-pane (ui/split :orientation :vertical :childs [result-pane locals-stack-tab-pane]) left-right-pane (ui/split :orientation :horizontal :childs [forms-pane locals-result-pane] :sizes [0.6])] left-right-pane)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/components.clj ================================================ (ns flow-storm.debugger.ui.flows.components (:require [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.ui.commons :refer [def-val]] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.data-windows.data-windows :as data-windows] [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup]]) (:import [javafx.scene.layout VBox Priority] [javafx.scene.control TextArea TextField])) (defn def-kind-colored-label [text kind] (case kind :defmethod (ui/label :text text :class "defmethod") :extend-protocol (ui/label :text text :class "extend-protocol") :extend-type (ui/label :text text :class "extend-type") :defn (ui/label :text text :class "defn") #_else (ui/label :text text :class "anonymous"))) (defn create-pprint-pane [flow-id thread-id pane-id] (let [^TextArea result-txt (ui/text-area :text "" :editable? false :class "monospaced") print-meta-chk (ui/check-box :selected? false) print-level-txt (ui/text-field :initial-text "5" :align :center :pref-width 50) print-wrap-chk (ui/check-box :on-change (fn [selected?] (.setWrapText result-txt selected?)) :selected? false) def-btn (ui/button :label "def" :classes ["def-btn" "btn-sm"] :tooltip "Define a reference to this value so it can be used from the repl.") inspect-btn (ui/button :label "ins" :classes ["def-btn" "btn-sm"] :tooltip "Open this value in the value inspector.") tap-btn (ui/button :label "tap" :classes ["def-btn" "btn-sm"] :tooltip "Tap this value as with tap>. Useful to send it to other inspectors like portal, REBL, Reveal, etc") tools-box (ui/h-box :childs [(ui/label :text "*print-level*") print-level-txt (ui/label :text "*print-meta*") print-meta-chk (ui/label :text "*print-wrap*") print-wrap-chk def-btn inspect-btn tap-btn] :spacing 3 :align :center-right) result-type-lbl (ui/label :text "") extra-lbl (ui/label :text "") header-box (ui/h-box :childs [result-type-lbl extra-lbl] :spacing 3) box (ui/v-box :childs [tools-box header-box result-txt])] (VBox/setVgrow result-txt Priority/ALWAYS) (store-obj flow-id thread-id (ui-utils/thread-pprint-type-lbl-id pane-id) result-type-lbl) (store-obj flow-id thread-id (ui-utils/thread-pprint-extra-lbl-id pane-id) extra-lbl) (store-obj flow-id thread-id (ui-utils/thread-pprint-area-id pane-id) result-txt) (store-obj flow-id thread-id (ui-utils/thread-pprint-level-txt-id pane-id) print-level-txt) (store-obj flow-id thread-id (ui-utils/thread-pprint-meta-chk-id pane-id) print-meta-chk) (store-obj flow-id thread-id (ui-utils/thread-pprint-def-btn-id pane-id) def-btn) (store-obj flow-id thread-id (ui-utils/thread-pprint-inspect-btn-id pane-id) inspect-btn) (store-obj flow-id thread-id (ui-utils/thread-pprint-tap-btn-id pane-id) tap-btn) box)) (defn update-pprint-pane [flow-id thread-id pane-id {:keys [val-ref extra-text class]} _] (let [[result-type-lbl] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-type-lbl-id pane-id)) [result-txt] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-area-id pane-id)) [print-level-txt] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-level-txt-id pane-id)) [print-meta-chk] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-meta-chk-id pane-id)) [def-btn] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-def-btn-id pane-id)) [inspect-btn] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-inspect-btn-id pane-id)) [tap-btn] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-tap-btn-id pane-id)) [extra-lbl] (obj-lookup flow-id thread-id (ui-utils/thread-pprint-extra-lbl-id pane-id)) {:keys [val-str val-type]} (when val-ref (runtime-api/val-pprint rt-api val-ref {:print-length 50 :print-level (Integer/parseInt (.getText ^TextField print-level-txt)) :print-meta? (ui-utils/checkbox-checked? print-meta-chk) :pprint? (:pprint-previews? (dbg-state/debugger-config))}))] (ui-utils/set-button-action def-btn (fn [] (def-val val-ref))) (ui-utils/set-button-action inspect-btn (fn [] (data-windows/create-data-window-for-vref val-ref))) (ui-utils/set-button-action tap-btn (fn [] (runtime-api/tap-value rt-api val-ref))) (ui-utils/set-text extra-lbl (or extra-text "")) (case class :warning (do (ui-utils/rm-class extra-lbl "fail") (ui-utils/add-class extra-lbl "warning")) :fail (do (ui-utils/rm-class extra-lbl "warning") (ui-utils/add-class extra-lbl "fail")) (do (ui-utils/rm-class extra-lbl "fail") (ui-utils/rm-class extra-lbl "warning"))) (ui-utils/set-text-input-text result-txt val-str) (ui-utils/set-text result-type-lbl (format "Type: %s" (or val-type ""))))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/functions.clj ================================================ (ns flow-storm.debugger.ui.flows.functions (:require [flow-storm.debugger.state :refer [store-obj obj-lookup] :as dbg-state] [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.ui.flows.general :as ui-flows-gral] [flow-storm.debugger.ui.flows.components :as flow-cmp] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.flows.code :as flows-code] [flow-storm.debugger.ui.tasks :as tasks] [clojure.pprint :refer [cl-format]] [clojure.string :as str] [flow-storm.debugger.ui.data-windows.data-windows :as data-windows]) (:import [javafx.scene.layout Priority HBox VBox])) (def max-args 9) (defn- update-function-calls [flow-id thread-id] (when-let [{:keys [form-id fn-ns fn-name]} (dbg-state/get-selected-function-list-fn flow-id thread-id)] (let [[{:keys [clear add-all]}] (obj-lookup flow-id thread-id "function_calls_list") _ (clear) [selected-args-fn] (obj-lookup flow-id thread-id "function_calls_selected_args_and_ret_fn") {:keys [sel-args ret?]} (selected-args-fn) render-args (when (not= (count sel-args) max-args) sel-args)] (tasks/submit-task runtime-api/collect-fn-frames-task [flow-id thread-id fn-ns fn-name form-id render-args ret?] {:on-progress (fn [{:keys [batch]}] (add-all batch))})))) (defn- functions-cell-factory [_ {:keys [cell-type] :as cell-info}] (case cell-type :calls (ui/h-box :childs [(ui/label :text (cl-format nil "~:d" (:cnt cell-info)))] :align :center-right) :function (let [{:keys [form-def-kind fn-name fn-ns dispatch-val]} cell-info fn-lbl (case form-def-kind :defmethod (flow-cmp/def-kind-colored-label (format "%s/%s %s" fn-ns fn-name (-> dispatch-val meta :val-preview)) form-def-kind) :extend-protocol (flow-cmp/def-kind-colored-label (format "%s/%s" fn-ns fn-name) form-def-kind) :extend-type (flow-cmp/def-kind-colored-label (format "%s/%s" fn-ns fn-name) form-def-kind) :defrecord (flow-cmp/def-kind-colored-label (format "%s/%s" fn-ns fn-name) form-def-kind) :deftype (flow-cmp/def-kind-colored-label (format "%s/%s" fn-ns fn-name) form-def-kind) :defn (flow-cmp/def-kind-colored-label (format "%s/%s" fn-ns fn-name) form-def-kind) (flow-cmp/def-kind-colored-label (format "%s/%s" fn-ns fn-name) form-def-kind))] fn-lbl))) (defn- uninstrument-items [items] (let [groups (->> items (group-by (fn [{:keys [form-def-kind]}] (cond (#{:defn} form-def-kind) :vars (#{:defmethod :extend-protocol :extend-type} form-def-kind) :forms :else nil))))] (let [vars-symbs (->> (:vars groups) (map (fn [{:keys [fn-name fn-ns]}] (symbol fn-ns fn-name))))] (doseq [vs vars-symbs] (runtime-api/vanilla-uninstrument-var rt-api (namespace vs) (name vs) {}))) (let [forms (->> (:forms groups) (map (fn [{:keys [fn-ns form]}] {:form-ns fn-ns :form form})))] (when (seq forms) (doseq [{:keys [form-ns form]} forms] (runtime-api/eval-form rt-api form {:instrument? false :ns form-ns})))))) (defn- function-click [mev selected-items {:keys [table-view-pane]}] ;; selected items contains rows like [{...fn-call...} cnt] (let [selected-items (map first selected-items)] (cond (and (ui-utils/mouse-secondary? mev) (not (dbg-state/clojure-storm-env?))) (let [ctx-menu-un-instrument-item {:text "Un-instrument seleced functions" :on-click (fn [] (uninstrument-items selected-items) )} ctx-menu (ui/context-menu :items [ctx-menu-un-instrument-item])] (ui-utils/show-context-menu :menu ctx-menu :parent table-view-pane :mouse-ev mev))))) (defn- create-fns-list-pane [flow-id thread-id] (let [{:keys [table-view-pane table-view] :as tv-data} (ui/table-view :columns ["Functions" "Calls"] :cell-factory functions-cell-factory :resize-policy :constrained :on-click function-click :on-selection-change (fn [_ sel-item] (dbg-state/set-selected-function-list-fn flow-id thread-id (first sel-item)) (update-function-calls flow-id thread-id)) :selection-mode :multiple :search-predicate (fn [[{:keys [fn-name fn-ns]} _] search-str] (str/includes? (format "%s/%s" fn-ns fn-name) search-str)))] (store-obj flow-id thread-id "functions_table_data" tv-data) (VBox/setVgrow table-view Priority/ALWAYS) table-view-pane)) (defn- functions-calls-cell-factory [list-cell {:keys [args-vec ret throwable args-vec-str ret-str throwable-str]}] (let [args-node (when-not (str/blank? args-vec-str) (ui/h-box :childs [(ui/button :label "args" :classes ["def-btn" "btn-sm"] :tooltip "Open this value in the value inspector." :on-click (fn [] (data-windows/create-data-window-for-vref args-vec))) (ui/label :text args-vec-str)] :spacing 5)) ret-node (when ret-str (ui/h-box :childs [(ui/button :label "ret" :classes ["def-btn" "btn-sm"] :tooltip "Open this value in the value inspector." :on-click (fn [] (data-windows/create-data-window-for-vref ret))) (ui/label :text ret-str)] :spacing 5)) throwable-node (when throwable-str (ui/h-box :childs [(ui/button :label "throw" :classes ["def-btn" "btn-sm"] :tooltip "Open this value in the value inspector." :on-click (fn [] (data-windows/create-data-window-for-vref throwable))) (ui/label :text throwable-str :class "fail")] :spacing 5)) ret-kind-node (cond ret-str ret-node throwable-str throwable-node) cell (ui/v-box :childs (cond-> [] args-node (conj args-node) ret-kind-node (conj ret-kind-node)) :class "fn-call-list-cell" :spacing 5 :paddings [5])] (ui-utils/set-graphic list-cell cell))) (defn- function-call-click [flow-id thread-id mev selected-items {:keys [list-view-pane]}] (let [idx (-> selected-items first :fn-call-idx) jump-to-idx (fn [] (ui-flows-gral/select-thread-tool-tab flow-id thread-id "flows-code-stepper") (flows-code/jump-to-coord flow-id thread-id (runtime-api/timeline-entry rt-api flow-id thread-id idx :at)))] (cond (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (jump-to-idx) (ui-utils/mouse-secondary? mev) (let [ctx-menu (ui/context-menu :items [{:text "Step code" :on-click jump-to-idx}])] (ui-utils/show-context-menu :menu ctx-menu :parent list-view-pane :mouse-ev mev))))) (defn- create-fn-calls-list-pane [flow-id thread-id] (let [args-checks (repeatedly max-args (fn [] (ui/check-box :on-change (fn [_] (update-function-calls flow-id thread-id)) :selected? true :focus-traversable? false))) {:keys [list-view-pane] :as lv-data} (ui/list-view :editable? false :cell-factory functions-calls-cell-factory :on-click (partial function-call-click flow-id thread-id) :selection-mode :single) args-print-type-checks (ui/h-box :childs (->> args-checks (map-indexed (fn [idx cb] (ui/h-box :childs [(ui/label :text (format "a%d" (inc idx))) cb]))) (into [(ui/label :text "Print args:")])) :spacing 8) ret-check (ui/check-box :on-change (fn [_] (update-function-calls flow-id thread-id)) :selected? true :focus-traversable? false) fn-call-list-pane (ui/v-box :childs [args-print-type-checks (ui/h-box :childs [(ui/label :text "Print ret?") ret-check]) list-view-pane] :spacing 5) selected-args-and-ret (fn [] {:sel-args (->> args-checks (keep-indexed (fn [idx cb] (when (ui-utils/checkbox-checked? cb) idx)))) :ret? (ui-utils/checkbox-checked? ret-check)})] (VBox/setVgrow list-view-pane Priority/ALWAYS) (store-obj flow-id thread-id "function_calls_list" lv-data) (store-obj flow-id thread-id "function_calls_selected_args_and_ret_fn" selected-args-and-ret) fn-call-list-pane)) (defn update-functions-pane [flow-id thread-id] (let [fn-call-stats (->> (runtime-api/fn-call-stats rt-api flow-id thread-id) (sort-by :cnt >) (map (fn [fn-call] [(assoc fn-call :cell-type :function) (assoc fn-call :cell-type :calls)]))) [{:keys [add-all clear]}] (obj-lookup flow-id thread-id "functions_table_data")] (clear) (add-all fn-call-stats))) (defn create-functions-pane [flow-id thread-id] (let [fns-list-pane (create-fns-list-pane flow-id thread-id) fn-calls-list-pane (create-fn-calls-list-pane flow-id thread-id) split-pane (ui/split :orientation :horizontal :childs [fns-list-pane fn-calls-list-pane])] (HBox/setHgrow fn-calls-list-pane Priority/ALWAYS) (VBox/setVgrow split-pane Priority/ALWAYS) (update-functions-pane flow-id thread-id) split-pane)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/general.clj ================================================ (ns flow-storm.debugger.ui.flows.general (:require [flow-storm.debugger.state :as dbg-state :refer [obj-lookup]] [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.components :as ui] [flow-storm.utils :as utils] [clojure.java.io :as io] [clojure.string :as str] [clojure.java.shell :as shell]) (:import [javafx.scene.control TabPane] [java.io File] [java.net URL])) (defn select-thread-tool-tab [flow-id thread-id tab-id] (let [[^TabPane thread-tools-tab-pane] (obj-lookup flow-id thread-id "thread_tool_tab_pane_id") sel-model (.getSelectionModel thread-tools-tab-pane) tab (some (fn [t] (when (= tab-id (.getId t )) t)) (.getTabs thread-tools-tab-pane))] (ui-utils/selection-select-obj sel-model tab) (.requestFocus thread-tools-tab-pane))) (defn select-main-tools-tab [tab-id] (let [[^TabPane main-tools-tab] (obj-lookup "main-tools-tab") sel-model (.getSelectionModel main-tools-tab) tab (some (fn [t] (when (= tab-id (.getId t )) t)) (.getTabs main-tools-tab))] (ui-utils/selection-select-obj sel-model tab))) (defn show-message [msg msg-type] (try (ui-utils/run-later (ui/alert-dialog :type msg-type :message msg :buttons [:close] :center-on-stage (dbg-state/main-jfx-stage) :height 200 :width 700)) (catch Exception _))) (defn open-form-in-editor ([form] (open-form-in-editor form nil)) ([form line] (try (let [form-file (:form/file form) file-path (try (let [url (or (io/resource form-file) (let [file (when-let [f (io/file form-file)] (and (.exists ^File f) f))] (.toURL ^File file)))] (.toExternalForm ^URL url)) (catch Exception _ nil))] (if-not file-path (show-message "There is no file info associated with this form. Maybe it was typed at the repl?" :warning) (let [editor-jar-pattern (System/getProperty "flowstorm.jarEditorCommand") editor-file-pattern (System/getProperty "flowstorm.fileEditorCommand") form-line (or line (some-> form :form/form meta :line)) ;; If form-file is inside a jar it, file-path will be like : ;; jar:file:/home/jmonetta/.m2/repository/org/clojure/data.codec/0.2.0/data.codec-0.2.0.jar!/clojure/data/codec/base64.clj ;; while if it is in your source directories it will be like : ;; file:/home/jmonetta/my-projects/flow-storm-debugger/src-dev/dev_tester.clj command (cond (str/starts-with? file-path "jar:file:/") (if editor-jar-pattern (let [[_ jar-path file-path] (re-find #"jar:file:(/.+\.jar)\!/(.+)" file-path)] (-> editor-jar-pattern (str/replace "<>" jar-path) (str/replace "<>" file-path) (str/replace "<>" (str (or form-line 0))))) (do (show-message "No editor set to open jar files. Please provide the jvm option flowstorm.jarEditorCommand. Refer to the user guide for more info." :info) nil)) (str/starts-with? file-path "file:/") (if editor-file-pattern (let [[_ file-path] (re-find #"file:(/.+)" file-path)] (-> editor-file-pattern (str/replace "<>" file-path) (str/replace "<>" (str (or form-line 0))))) (do (show-message "No editor set to open files. Please provide the jvm option flowstorm.fileEditorCommand. Refer to the user guide for more info." :info) nil)) :else (throw (Exception. (str "Don't know how to open this file " form-file))))] (when command (utils/log (str "Running : " command)) (apply shell/sh (utils/quoted-string-split command \space)))))) (catch Exception e (utils/log-error (.getMessage e)))))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/multi_thread_timeline.clj ================================================ (ns flow-storm.debugger.ui.flows.multi-thread-timeline (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.runtime-api :as rt-api :refer [rt-api]] [clojure.string :as str] [clojure.set :as set] [flow-storm.debugger.state :as dbg-state :refer [obj-lookup store-obj]] [flow-storm.debugger.events-queue :as events-queue]) (:import [javafx.scene.layout Priority VBox] [javafx.scene.control TableRow Label] [javafx.scene Scene] [javafx.stage Stage])) (def thread-possible-colors #{"#DAE8FC" "#D5E8D4" "#FFE6CC" "#F8CECC" "#E1D5E7" "#60A917" "#d45757" "#30cfcf" "#ed55e8" "#d3d929"}) (defn clear-timeline-ui [flow-id] (when-let [[{:keys [clear]}] (obj-lookup flow-id "total-order-table-data")] (clear))) (defn- main-pane [flow-id] (let [{:keys [table-view-pane table-view add-all clear] :as table-data} (ui/table-view :columns ["Thread" "Thread Idx" "Function" "Expression" "Value" "Value type"] :resize-policy :constrained :cell-factory (fn [_ cell-val] (doto ^Label (ui/label :text (str cell-val)) (.setStyle "-fx-text-fill: #333"))) :row-update (fn [^TableRow trow row-vec] (doto trow (.setStyle (format "-fx-background-color: %s" (-> row-vec meta :color))) (.setOnMouseClicked (event-handler [mev] (when (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (let [{:keys [thread-id thread-timeline-idx]} (meta row-vec) goto-loc (requiring-resolve 'flow-storm.debugger.ui.flows.screen/goto-location)] (goto-loc {:flow-id flow-id :thread-id thread-id :idx thread-timeline-idx}))))))) :search-predicate (fn [[thread-name _ function expr-str expr-val expr-type] search-str] (boolean (or (str/includes? thread-name search-str) (str/includes? function search-str) (str/includes? expr-str search-str) (and expr-val (str/includes? expr-val search-str)) (and expr-type (str/includes? expr-type search-str))))) :items []) only-functions-cb (ui/check-box :selected? false) refresh (fn [] (clear) (let [thread-selected-colors (atom {}) params {:only-functions? (ui-utils/checkbox-checked? only-functions-cb) :flow-id flow-id} timeline-task-id (rt-api/total-order-timeline-task rt-api params) thread-color (fn [thread-id] (if-let [color (get @thread-selected-colors thread-id)] color (let [new-color (first (set/difference thread-possible-colors (into #{} (vals @thread-selected-colors))))] (swap! thread-selected-colors assoc thread-id (or new-color ;; in case we run out of colors (first thread-possible-colors))) new-color)))] (events-queue/add-dispatch-fn :tote-timeline (fn [[ev-type {:keys [task-id batch]}]] (when (= timeline-task-id task-id) (case ev-type :task-progress (ui-utils/run-later (->> batch (mapv (fn [{:keys [thread-timeline-idx type thread-id fn-ns fn-name expr-str expr-type expr-val-str] :as tl-entry}] (let [{:keys [thread/name]} (dbg-state/get-thread-info thread-id) idx thread-timeline-idx] (with-meta (case type :fn-call [(ui/thread-label thread-id name) idx (format "%s/%s" fn-ns fn-name) "" "" ""] :fn-return [(ui/thread-label thread-id name) idx "RETURN" "" expr-val-str expr-type] :fn-unwind [(ui/thread-label thread-id name) idx "UNWIND" "" "" expr-type] :expr-exec [(ui/thread-label thread-id name) idx "" expr-str expr-val-str expr-type]) (assoc tl-entry :color (thread-color thread-id)))))) add-all)) :task-finished (events-queue/rm-dispatch-fn :tote-timeline) nil)))) (rt-api/start-task rt-api timeline-task-id))) refresh-btn (ui/icon-button :icon-name "mdi-reload" :on-click refresh :tooltip "Refresh the content of the timeline") main-pane (ui/border-pane :top (ui/v-box :childs [(ui/label :text (format "Flow: %d" flow-id)) (ui/h-box :childs [refresh-btn (ui/label :text "Only functions? :") only-functions-cb] :class "controls-box" :spacing 5)]) :center table-view-pane :class "timeline-tool")] (store-obj flow-id "total-order-table-data" table-data) (VBox/setVgrow table-view Priority/ALWAYS) (refresh) main-pane)) (defn open-timeline-window [flow-id] (let [window-w 1000 window-h 1000 scene (Scene. (main-pane flow-id) window-w window-h) stage (doto (Stage.) (.setTitle "FlowStorm multi-thread timeline browser") (.setScene scene))] (.setOnCloseRequest stage (event-handler [_] (dbg-state/unregister-jfx-stage! stage))) (dbg-state/register-jfx-stage! stage) (let [{:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) window-w window-h)] (.setX stage x) (.setY stage y)) (-> stage .show))) (comment ) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/printer.clj ================================================ (ns flow-storm.debugger.ui.flows.printer (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui] [flow-storm.utils :as utils] [flow-storm.debugger.ui.tasks :as tasks] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.flows.general :refer [show-message]] [clojure.string :as str] [flow-storm.debugger.state :as dbg-state :refer [obj-lookup store-obj]]) (:import [javafx.scene.layout Priority VBox] [javafx.scene.control ComboBox] [javafx.scene Scene] [javafx.stage Stage])) (defn- make-printer-print-outs-list-id [flow-id] (format "printer-print-outs-list-%s" flow-id)) (defn- make-printer-prints-controls-table-id [flow-id] (format "printer-prints-controls-table-%s" flow-id)) (defn clear-prints-ui [flow-id] (when-let [[{:keys [clear]}] (obj-lookup (make-printer-print-outs-list-id flow-id))] (clear))) (defn update-prints-controls [flow-id] (when-let [[{:keys [clear add-all]}] (obj-lookup (make-printer-prints-controls-table-id flow-id))] (let [printers-rows (->> (dbg-state/printers flow-id) (reduce-kv (fn [r _ frm-printers] (reduce-kv (fn [rr _ p] (conj rr p)) r frm-printers)) []) (mapv (fn [printer] [(assoc printer :cell-type :function) (assoc printer :cell-type :source-expr) (assoc printer :cell-type :print-level) (assoc printer :cell-type :print-length) (assoc printer :cell-type :format) (assoc printer :cell-type :transform-expr) (assoc printer :cell-type :enable?) (assoc printer :cell-type :action)])))] (clear) (add-all printers-rows)))) (defn build-prints-controls [flow-id] (let [{:keys [table-view-pane] :as table-data} (ui/table-view :columns ["Function" "Expr" "*print-level*" "*print-length*" "Format" "Transform Fn" "Enable" "_"] :resize-policy :constrained :cell-factory (fn [_ {:keys [form/id coord cell-type fn-ns fn-name transform-expr-str source-expr print-level print-length format-str enable?]}] (case cell-type :function (ui/label :text (format "%s/%s" fn-ns fn-name)) :source-expr (ui/label :text (pr-str source-expr)) :print-level (ui/text-field :initial-text (str print-level) :on-change (fn [val] (dbg-state/update-printer flow-id id coord :print-level (Long/parseLong val))) :align :center-right) :print-length (ui/text-field :initial-text (str print-length) :on-change (fn [val] (dbg-state/update-printer flow-id id coord :print-length (Long/parseLong val))) :align :center-right) :format (ui/text-field :initial-text format-str :on-change (fn [val] (dbg-state/update-printer flow-id id coord :format-str val)) :align :center-left) :transform-expr (ui/text-field :initial-text transform-expr-str :on-change (fn [val] (dbg-state/update-printer flow-id id coord :transform-expr-str val)) :align :center-left) :enable? (ui/check-box :on-change (fn [selected?] (dbg-state/update-printer flow-id id coord :enable? selected?)) :selected? enable?) :action (ui/icon-button :icon-name "mdi-delete-forever" :on-click (fn [] (dbg-state/remove-printer flow-id id coord) (update-prints-controls flow-id))))) :items [])] ;; Hacky, we store the obj as a global instead of under flow-id so the printers don't need to be redefined after flow-cleanning (store-obj (make-printer-prints-controls-table-id flow-id) table-data) table-view-pane)) (defn- prepare-printers [printers] (utils/update-values printers (fn [form-printers] (reduce-kv (fn [r coord {:keys [enable?] :as printer}] (if enable? (let [p (update printer :format-str (fn [fs] (if (str/includes? fs "%s") fs (str fs " %s"))))] (assoc r coord p)) r)) {} form-printers)))) (defn- main-pane [flow-id] (let [selected-thread-id (atom nil) ^ComboBox thread-id-combo (ui/combo-box :items (let [flows-threads (runtime-api/all-flows-threads rt-api)] (->> (keep (fn [[fid tid]] (when (= fid flow-id) {:thread-id tid :thread-name (:thread/name (dbg-state/get-thread-info tid))})) flows-threads) (into [{:thread-name "All"}]))) :cell-factory (fn [_ {:keys [thread-id thread-name]}] (ui/label :text (if thread-id (ui/thread-label thread-id thread-name) thread-name))) :button-factory (fn [_ {:keys [thread-id thread-name]}] (ui/label :text (if thread-id (ui/thread-label thread-id thread-name) thread-name))) :on-change (fn [_ {:keys [thread-id]}] (reset! selected-thread-id thread-id))) {:keys [list-view-pane list-view add-all clear] :as list-data} (ui/list-view :editable? false :cell-factory (fn [list-cell {:keys [thread-id text]}] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :text text :tooltip (format "ThreadId: %s" thread-id))))) :on-click (fn [mev sel-items _] (when (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (let [{:keys [idx thread-id]} (first sel-items) goto-loc (requiring-resolve 'flow-storm.debugger.ui.flows.screen/goto-location)] (goto-loc {:flow-id flow-id :thread-id thread-id :idx idx})))) :selection-mode :single :search-predicate (fn [{:keys [text]} search-str] (str/includes? text search-str))) refresh-btn (ui/icon-button :icon-name "mdi-reload" :on-click (fn [] (let [thread-id @selected-thread-id] (clear) (when (and (nil? thread-id) (zero? (runtime-api/multi-thread-timeline-count rt-api flow-id))) (show-message "If you don't select any threads and you haven't recorded on the multi-thread timeline the prints will be sorted by thread." :warning)) (tasks/submit-task runtime-api/thread-prints-task [(cond-> {:printers (prepare-printers (dbg-state/printers flow-id)) :flow-id flow-id} thread-id (assoc :thread-id thread-id))] {:on-progress (fn [{:keys [batch]}] (add-all (into [] (map (fn [po] (assoc po :flow-id flow-id))) batch)))}))) :tooltip "Re print everything") prints-controls-pane (build-prints-controls flow-id) thread-box (ui/h-box :childs [(ui/label :text "Thread id:") thread-id-combo] :spacing 5) split-pane (ui/split :orientation :vertical :childs [prints-controls-pane list-view-pane] :sizes [0.3]) main-pane (ui/border-pane :top (ui/v-box :childs [(ui/label :text (format "Flow: %d" flow-id)) (ui/h-box :childs [refresh-btn thread-box] :class "controls-box" :spacing 5)]) :center split-pane :class "printer-tool" :paddings [10 10 10 10])] (store-obj flow-id "printer-thread-id-combo" thread-id-combo) ;; Hacky, we store the obj as a global instead of under flow-id so the printers don't need to be redefined after flow-cleanning (store-obj (make-printer-print-outs-list-id flow-id) list-data) (VBox/setVgrow list-view Priority/ALWAYS) main-pane)) (defn open-printers-window [flow-id] (let [window-w 1000 window-h 1000 scene (Scene. (main-pane flow-id) window-w window-h) stage (doto (Stage.) (.setTitle "FlowStorm printers") (.setScene scene))] (.setOnCloseRequest stage (event-handler [_] (dbg-state/unregister-jfx-stage! stage))) (dbg-state/register-jfx-stage! stage) (let [{:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) window-w window-h)] (.setX stage x) (.setY stage y)) (update-prints-controls flow-id) (-> stage .show))) (comment ) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/screen.clj ================================================ (ns flow-storm.debugger.ui.flows.screen (:require [flow-storm.debugger.ui.flows.code :as flow-code] [flow-storm.debugger.ui.flows.general :as ui-general] [flow-storm.debugger.ui.flows.call-tree :as flow-tree] [flow-storm.debugger.ui.flows.functions :as flow-fns] [flow-storm.debugger.ui.flows.search :as search] [flow-storm.debugger.ui.flows.bookmarks :as bookmarks] [flow-storm.debugger.ui.flows.multi-thread-timeline :as multi-thread-timeline] [flow-storm.debugger.ui.flows.printer :as printer] [flow-storm.debugger.ui.tasks :as tasks] [flow-storm.debugger.ui.plugins :as plugins] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler key-combo-match?]] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup clean-objs]]) (:import [javafx.scene.control Tab TabPane ListView] [javafx.scene.layout Pane VBox Priority] [javafx.scene.input KeyEvent])) (declare create-or-focus-thread-tab) (declare clear-and-hide-exception-menu) (defn clear-debugger-flow [flow-id] (let [[flows-tabs-pane] (obj-lookup "flows_tabs_pane") [flow-tab] (obj-lookup flow-id "flow_tab")] (when (and flows-tabs-pane flow-tab) (ui-utils/rm-tab-pane-tab flows-tabs-pane flow-tab)) ;; remove all bookmarks, mt-timeline and prints ui associated to this flow (bookmarks/remove-bookmarks flow-id) (multi-thread-timeline/clear-timeline-ui flow-id) (printer/clear-prints-ui flow-id) (clear-and-hide-exception-menu flow-id) (dbg-state/remove-flow flow-id) ;; clean ui state objects (clean-objs flow-id) ;; notify all plugins (doseq [{:keys [plugin/on-flow-clear plugin/create-result]} (plugins/plugins)] (when on-flow-clear (on-flow-clear flow-id create-result))))) (defn discard-all-flows [] (doseq [fid (dbg-state/all-flows-ids)] (runtime-api/discard-flow rt-api fid))) (defn- setup-thread-keybindngs [flow-id thread-id pane] (.setOnKeyPressed ^Pane pane (event-handler [^KeyEvent kev] (let [key-txt (.getText kev)] (cond (= key-txt "t") (ui-general/select-thread-tool-tab flow-id thread-id "flows-call-tree") (= key-txt "c") (ui-general/select-thread-tool-tab flow-id thread-id "flows-code-stepper") (= key-txt "f") (ui-general/select-thread-tool-tab flow-id thread-id "flows-functions") (= key-txt "P") (flow-code/step-prev-over flow-id thread-id) (= key-txt "p") (flow-code/step-prev flow-id thread-id) (= key-txt "n") (flow-code/step-next flow-id thread-id) (= key-txt "N") (flow-code/step-next-over flow-id thread-id) (= key-txt "^") (flow-code/step-out flow-id thread-id) (= key-txt "<") (flow-code/step-first flow-id thread-id) (= key-txt ">") (flow-code/step-last flow-id thread-id) (key-combo-match? kev "f" [:ctrl :shift]) (flow-code/copy-current-frame-symbol flow-id thread-id true) (key-combo-match? kev "f" [:ctrl]) (flow-code/copy-current-frame-symbol flow-id thread-id false) (key-combo-match? kev "z" [:ctrl]) (flow-code/undo-jump flow-id thread-id) (key-combo-match? kev "r" [:ctrl]) (flow-code/redo-jump flow-id thread-id)))))) (defn open-thread [thread-info] (let [flow-id (:flow/id thread-info) thread-id (:thread/id thread-info) thread-name (:thread/name thread-info)] (when-not (dbg-state/get-thread flow-id thread-id) (dbg-state/create-thread flow-id thread-id)) (create-or-focus-thread-tab flow-id thread-id thread-name) (when-let [tl-entry (runtime-api/timeline-entry rt-api flow-id thread-id 0 :at)] (flow-code/jump-to-coord flow-id thread-id tl-entry)) (ui-general/select-thread-tool-tab flow-id (:thread/id thread-info) "flows-code-stepper"))) (defn update-outdated-thread-ui [flow-id thread-id] (when (:call-tree-update? (dbg-state/debugger-config)) (flow-tree/update-call-stack-tree-pane flow-id thread-id)) (flow-fns/update-functions-pane flow-id thread-id) (flow-code/update-thread-trace-count-lbl flow-id thread-id)) (defn make-outdated-thread [flow-id thread-id] (when-let [[^Tab tab] (obj-lookup flow-id thread-id "tab")] (let [th-info (dbg-state/get-thread-info thread-id) thread-label (ui/thread-label (:thread/id th-info) (:thread/name th-info)) refresh-tab-content (ui/h-box :childs [(ui/label :text thread-label) (ui/icon-button :icon-name "mdi-reload" :tooltip "There are new recordings for this thread, click this button to update the UI." :on-click (fn [] (update-outdated-thread-ui flow-id thread-id) (doto tab (.setText thread-label) (.setGraphic nil))) :classes ["thread-refresh" "btn-sm"])])] (doto tab (.setText nil) (.setGraphic refresh-tab-content))))) (defn update-blocked-threads [] (try (let [[{:keys [set-items clear-items]}] (obj-lookup "blocked-threads-menu-data") [blocked-threads-box] (obj-lookup "blocked-threads-box") blocked-threads (->> (dbg-state/get-threads-info) (filter :thread/blocked))] (if (seq blocked-threads) (do (ui-utils/rm-class blocked-threads-box "hidden-node") (->> blocked-threads (mapv (fn [bthread] (let [[bp-var-ns bp-var-name] (:thread/blocked bthread)] {:thread-id (:thread/id bthread) :text (format "Unblock %s - %s/%s" (ui/thread-label (:thread/id bthread) (:thread/name bthread)) bp-var-ns bp-var-name)}))) (into [{:thread-id :all :text "Unblock all threads"}]) set-items)) (do (clear-items) (ui-utils/add-class blocked-threads-box "hidden-node")))) (catch Exception e (.printStackTrace e)))) (defn update-threads-list ([flow-id] (update-threads-list flow-id (runtime-api/flow-threads-info rt-api flow-id))) ([flow-id threads-info] (doseq [tinfo threads-info] (dbg-state/update-thread-info (:thread/id tinfo) tinfo)) (let [[{:keys [set-items menu-button] :as menu-data}] (obj-lookup flow-id "flow_threads_menu")] (when menu-data (let [[threads-tabs-pane] (obj-lookup flow-id "threads_tabs_pane")] (when (and (seq threads-info) threads-tabs-pane (zero? (count (.getTabs threads-tabs-pane)))) (open-thread (first threads-info))) (set-items threads-info) (.setText menu-button (format "Threads [%d]" (count threads-info)))))) (update-blocked-threads))) (defn select-flow-tab [flow-id] (let [[^TabPane flows-tabs-pane] (obj-lookup "flows_tabs_pane") sel-model (.getSelectionModel flows-tabs-pane) all-tabs (.getTabs flows-tabs-pane) tab-for-flow (some (fn [^Tab t] (when (= (.getId t) (str "flow-tab-" flow-id)) t)) all-tabs)] ;; select the flow tab (when tab-for-flow (ui-utils/selection-select-obj sel-model tab-for-flow) ;; focus the threads list (let [[{:keys [^ListView list-view]}] (obj-lookup flow-id "flow_threads_list")] (when list-view (let [list-selection (.getSelectionModel list-view)] (.requestFocus list-view) (ui-utils/selection-select-first list-selection))))))) (defn goto-location [{:keys [flow-id thread-id idx]}] (ui-general/select-main-tools-tab "tool-flows") (select-flow-tab flow-id) (open-thread (assoc (dbg-state/get-thread-info thread-id) :flow/id flow-id)) (ui-general/select-thread-tool-tab flow-id thread-id "flows-code-stepper") (flow-code/jump-to-coord flow-id thread-id (runtime-api/timeline-entry rt-api flow-id thread-id idx :at))) (defn- build-flow-tool-bar-pane [flow-id] (let [quick-jump-autocomplete (ui/autocomplete-textfield :get-completions (fn [] (into [] (keep (fn [{:keys [thread-id fn-ns fn-name cnt]}] (when-not (re-find #"fn--[\d]+$" fn-name) (let [th-info (dbg-state/get-thread-info thread-id)] {:text (format "%s/%s [%d] %s" fn-ns fn-name cnt (ui/thread-label (:thread/id th-info) (:thread/name th-info))) :on-select (fn [] (tasks/submit-task runtime-api/find-fn-call-task [(symbol fn-ns fn-name) 0 {:flow-id flow-id :thread-id thread-id}] {:on-finished (fn [{:keys [result]}] (when result (goto-location (assoc result :thread-id thread-id :flow-id flow-id))))}))})))) (runtime-api/fn-call-stats rt-api flow-id nil)))) quick-jump-textfield (ui/h-box :childs [(ui/label :text "Quick jump:") quick-jump-autocomplete] :align :center-left) blocked-threads-menu-data (ui/menu-button :title "Threads blocked" :on-action (fn [{:keys [thread-id]}] (if (= thread-id :all) (runtime-api/unblock-all-threads rt-api) (runtime-api/unblock-thread rt-api thread-id))) :items [] :class "important-combo") exceptions-menu-data (ui/menu-button :title "Exceptions" :on-action (fn [loc] (goto-location loc)) :items [] :class "important-combo") exceptions-box (ui/h-box :childs [(:menu-button exceptions-menu-data)] :class "hidden-node" :align :center-left) blocked-threads-box (ui/h-box :childs [(:menu-button blocked-threads-menu-data)] :class "hidden-node" :align :center-left) tools-menu (ui/menu-button :title "More tools" :items [{:key :search :text "Search"} {:key :multi-thread-timeline :text "Multi-thread timeline browser"} {:key :printers :text "Printers"}] :on-action (fn [item] (case (:key item) :search (search/search-window flow-id) :multi-thread-timeline (multi-thread-timeline/open-timeline-window flow-id) :printers (printer/open-printers-window flow-id))) :orientation :right-to-left) left-tools-box (ui/h-box :childs [quick-jump-textfield exceptions-box blocked-threads-box] :spacing 4) right-tools-box (ui/h-box :childs [(:menu-button tools-menu)] :spacing 4)] (store-obj flow-id "exceptions-box" exceptions-box) (store-obj flow-id "exceptions-menu-data" exceptions-menu-data) (store-obj "blocked-threads-box" blocked-threads-box) (store-obj "blocked-threads-menu-data" blocked-threads-menu-data) (ui/border-pane :left left-tools-box :right right-tools-box :paddings [5 5 0 5]))) (defn create-empty-flow [flow-id] (let [[flows-tabs-pane] (obj-lookup "flows_tabs_pane") threads-tab-pane (ui/tab-pane :closing-policy :all-tabs :drag-policy :reorder :class "threads-tab-pane") {:keys [menu-button] :as menu-btn-data} (ui/menu-button :title "Threads" :on-action (fn [th] (open-thread th)) :item-factory (fn [{:keys [thread/name thread/id]}] (ui/label :text (ui/thread-label id name))) :class "hl-combo") flow-toolbar (build-flow-tool-bar-pane flow-id) flow-anchor (ui/anchor-pane :childs [{:node threads-tab-pane :top-anchor 5.0 :left-anchor 5.0 :right-anchor 5.0 :bottom-anchor 5.0} {:node menu-button :top-anchor 8.0 :left-anchor 10.0}]) flow-box (ui/v-box :childs [flow-toolbar flow-anchor]) flow-tab (ui/tab :id (str "flow-tab-" flow-id) :text (str "flow-" flow-id) :content flow-box)] (VBox/setVgrow flow-anchor Priority/ALWAYS) (VBox/setVgrow flow-box Priority/ALWAYS) (.setOnCloseRequest ^Tab flow-tab (event-handler [ev] (runtime-api/discard-flow rt-api flow-id) ;; since we are destroying this tab, we don't need ;; this event to propagate anymore (ui-utils/consume ev))) (store-obj flow-id "threads_tabs_pane" threads-tab-pane) (store-obj flow-id "flow_tab" flow-tab) (store-obj flow-id "flow_threads_menu" menu-btn-data) (update-threads-list flow-id) (ui-utils/add-tab-pane-tab flows-tabs-pane flow-tab))) (defn- create-thread-pane [flow-id thread-id] (let [code-stepper-tab (ui/tab :graphic (ui/icon :name "mdi-code-parentheses") :content (flow-code/create-code-pane flow-id thread-id) :tooltip "Code tool. Allows you to step over the traced code." :id "flows-code-stepper") callstack-tree-tab (ui/tab :graphic (ui/icon :name "mdi-file-tree") :content (flow-tree/create-call-stack-tree-pane flow-id thread-id) :tooltip "Call tree tool. Allows you to explore the recorded execution tree." :id "flows-call-tree") functions-tab (ui/tab :graphic (ui/icon :name "mdi-format-list-numbers") :content (flow-fns/create-functions-pane flow-id thread-id) :tooltip "Functions list tool. Gives you a list of all function calls and how many time they have been called." :id "flows-functions") thread-tools-tab-pane (ui/tab-pane :tabs [code-stepper-tab callstack-tree-tab functions-tab] :side :bottom :closing-policy :unavailable)] (store-obj flow-id thread-id "thread_tool_tab_pane_id" thread-tools-tab-pane) thread-tools-tab-pane)) (defn create-or-focus-thread-tab [flow-id thread-id thread-name] (let [[^TabPane threads-tabs-pane] (obj-lookup flow-id "threads_tabs_pane") sel-model (.getSelectionModel threads-tabs-pane) all-tabs (.getTabs threads-tabs-pane) tab-for-thread (some (fn [t] (when (= (.getId ^Tab t) (str thread-id)) t)) all-tabs)] (if tab-for-thread (ui-utils/selection-select-obj sel-model tab-for-thread) (let [thread-tab-pane (create-thread-pane flow-id thread-id) thread-tab (ui/tab :text (ui/thread-label thread-id thread-name) :content thread-tab-pane :id (str thread-id))] (setup-thread-keybindngs flow-id thread-id thread-tab-pane) (.setOnCloseRequest ^Tab thread-tab (event-handler [ev] (clean-objs flow-id thread-id) (dbg-state/remove-thread flow-id thread-id))) (ui-utils/add-tab-pane-tab threads-tabs-pane thread-tab) (store-obj flow-id thread-id "tab" thread-tab) (ui-utils/selection-select-obj sel-model thread-tab))))) (defn clear-and-hide-exception-menu [flow-id] (let [[{:keys [clear-items]}] (obj-lookup flow-id "exceptions-menu-data") [ex-box] (obj-lookup flow-id "exceptions-box")] (when ex-box (ui-utils/clear-classes ex-box) (ui-utils/add-class ex-box "hidden-node") (clear-items)))) (defn add-exception-to-menu [{:keys [flow-id thread-id idx fn-ns fn-name ex-type ex-message]}] (let [[{:keys [add-item]}] (obj-lookup flow-id "exceptions-menu-data") [ex-box] (obj-lookup flow-id "exceptions-box")] (when ex-box (ui-utils/clear-classes ex-box) (add-item {:text (format "%d - %s/%s %s" idx fn-ns fn-name ex-type) :tooltip ex-message :flow-id flow-id :thread-id thread-id :idx idx})))) (defn set-recording-btn [recording?] (ui-utils/run-later (let [[record-btn] (obj-lookup "record-btn")] (ui-utils/update-button-icon record-btn (if recording? "mdi-pause" "mdi-record"))))) (defn set-multi-timeline-recording-btn [recording?] (ui-utils/run-later (let [[btn] (obj-lookup "multi-timeline-record-btn")] (ui-utils/update-button-icon btn (if recording? ["mdi-chart-timeline" "mdi-pause"] ["mdi-chart-timeline" "mdi-record"]))))) (defn main-pane [] (let [flows-tpane (ui/tab-pane :closing-policy :all-tabs :side :top :class "flows-tab-pane") flows-combo (ui/combo-box :items (into [] (range 10)) :button-factory (fn [_ i] (ui/label :text (str "Rec on flow-" i))) :cell-factory (fn [_ i] (ui/label :text (str "flow-" i))) :on-change (fn [_ new-flow-id] (runtime-api/switch-record-to-flow rt-api new-flow-id)) :classes ["hl-combo" "flows-combo"]) clear-btn (ui/icon-button :icon-name "mdi-delete-forever" :tooltip "Clean all flows (Ctrl-l)" :on-click (fn [] (discard-all-flows))) record-btn (ui/icon-button :icon-name "mdi-record" :tooltip "Start/Stop recording" :on-click (fn [] (runtime-api/toggle-recording rt-api)) :classes ["record-btn"]) multi-timeline-record-btn (ui/icon-button :icon-name ["mdi-chart-timeline" "mdi-record"] :tooltip "Start/Stop recording of the multi-thread timeline" :on-click (fn [] (runtime-api/toggle-multi-timeline-recording rt-api))) record-controls (ui/h-box :childs [clear-btn record-btn multi-timeline-record-btn flows-combo] :paddings [4 4 4 4] :spacing 4) flow-anchor (ui/anchor-pane :childs [{:node flows-tpane :top-anchor 5.0 :left-anchor 5.0 :right-anchor 5.0 :bottom-anchor 5.0} {:node record-controls :top-anchor 8.0 :left-anchor 10.0}]) flows-box (ui/v-box :childs [flow-anchor])] (VBox/setVgrow flow-anchor Priority/ALWAYS) (VBox/setVgrow flows-box Priority/ALWAYS) (store-obj "record-btn" record-btn) (store-obj "multi-timeline-record-btn" multi-timeline-record-btn) (store-obj "flows_tabs_pane" flows-tpane) flows-box)) ================================================ FILE: src-dbg/flow_storm/debugger/ui/flows/search.clj ================================================ (ns flow-storm.debugger.ui.flows.search (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler]] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup]] [flow-storm.debugger.ui.tasks :as tasks] [flow-storm.debugger.runtime-api :as rt-api :refer [rt-api]]) (:import [javafx.scene Scene] [javafx.stage Stage] [javafx.scene.control Label] [javafx.scene.layout VBox Priority])) (defn search [{:keys [flow-id print-level print-length] :as criteria}] (let [[{:keys [add-all clear]}] (obj-lookup flow-id "search_results_table_data")] (clear) (tasks/submit-task rt-api/search-collect-timelines-entries-task [criteria {:print-level (Integer/parseInt (or print-level "3")) :print-length (Integer/parseInt (or print-length "3"))}] {:on-progress (fn [{:keys [batch]}] (add-all (->> batch (mapv (fn [entry] [(assoc entry :cell-type :thread-id) (assoc entry :cell-type :idx) (assoc entry :cell-type :preview)])))))}))) (defn create-search-params-pane [flow-id] (let [criteria (atom {:flow-id flow-id}) thread-combo (ui/combo-box :items (let [flows-threads (rt-api/all-flows-threads rt-api)] (->> (keep (fn [[fid tid]] (when (= fid flow-id) {:thread-id tid :thread-name (:thread/name (dbg-state/get-thread-info tid))})) flows-threads) (into [{:thread-name "All"}]))) :cell-factory (fn [_ {:keys [thread-id thread-name]}] (ui/label :text (if thread-id (ui/thread-label thread-id thread-name) thread-name))) :button-factory (fn [_ {:keys [thread-id thread-name]}] (ui/label :text (if thread-id (ui/thread-label thread-id thread-name) thread-name))) :on-change (fn [_ {:keys [thread-id]}] (if thread-id (swap! criteria assoc :thread-id thread-id) (swap! criteria dissoc :thread-id)))) search-txt (ui/text-field :prompt-text "Search") ;; Pr-str search pr-len-txt (ui/text-field :initial-text "3" (:print-length @criteria)) pr-lvl-txt (ui/text-field :initial-text "3" (:print-level @criteria)) pr-str-params-box (let [search-btn (ui/button :label "Search" :on-click (fn [] (search (assoc @criteria :search-type :pr-str :print-length (.getText pr-len-txt) :print-level (.getText pr-lvl-txt) :query-str (.getText search-txt)))))] (ui/v-box :childs [search-txt (ui/h-box :childs [(ui/label :text "*print-level*") pr-lvl-txt]) (ui/h-box :childs [(ui/label :text "*print-length*") pr-len-txt]) search-btn] :spacing 5 :paddings [10])) ;; Custom pred search pred-area (ui/text-area :text "(fn [v] (map? v))" :editable? true :on-change (fn [new-txt] (swap! criteria assoc :predicate-code-str new-txt))) custom-pred-params-box (let [search-btn (ui/button :label "Search" :on-click (fn [] (search (assoc @criteria :predicate-code-str (.getText pred-area) :search-type :custorm-predicate))))] (ui/v-box :childs [(ui/label :text "Custom predicate :") pred-area search-btn] :spacing 5 :paddings [10])) ;; Data window search dw-id-combo (ui/combo-box :items (keys (dbg-state/data-windows)) :cell-factory (fn [_ dw-id] (ui/label :text (str dw-id))) :button-factory (fn [_ dw-id] (ui/label :text (str dw-id)))) dw-search-box (let [search-btn (ui/button :label "Search" :on-click (fn [] (let [sel-dw-id (ui-utils/combo-box-get-selected-item dw-id-combo) dw-val-ref (some-> (dbg-state/data-window sel-dw-id) :stack first :val-data :flow-storm.runtime.values/val-ref)] (when dw-val-ref (search (assoc @criteria :search-type :val-identity :val-ref dw-val-ref))))))] (ui/v-box :childs [(ui/h-box :childs [(ui/label :text "Search value selected in data window id:") dw-id-combo]) search-btn] :spacing 5 :paddings [10])) search-tabs (ui/tab-pane :closing-policy :unavailable :tabs (cond-> [(ui/tab :text "By pr-str" :content pr-str-params-box :tooltip "Search by sub strings inside the pr-str of values") (ui/tab :text "Data window val" :content dw-search-box :tooltip "Search for values matching a predicate")] (= :clj (dbg-state/env-kind)) (conj (ui/tab :text "By predicate" :content custom-pred-params-box :tooltip "Search for values matching a predicate")))) gral-row-box (ui/h-box :childs [(ui/label :text "Thread:") thread-combo] :spacing 5)] (ui/border-pane :top gral-row-box :center search-tabs :paddings [10]))) (defn create-results-table-pane [flow-id] (let [{:keys [table-view-pane table-view] :as tv-data} (ui/table-view :columns ["Thread Id" "Index" "Preview"] :columns-width-percs [0.1 0.1 0.7] :cell-factory (fn [_ item] (let [^Label lbl (ui/label :text (case (:cell-type item) :thread-id (str (:thread-id item)) :idx (str (:idx item)) :preview (str (:entry-preview item))))] (.setMaxHeight lbl 50) lbl)) :resize-policy :constrained :selection-mode :single :on-click (fn [mev items _] (when (and (ui-utils/mouse-primary? mev) (ui-utils/double-click? mev)) (let [{:keys [thread-id idx]} (ffirst items) goto-loc (requiring-resolve 'flow-storm.debugger.ui.flows.screen/goto-location)] (goto-loc {:flow-id flow-id :thread-id thread-id :idx idx})))))] (store-obj flow-id "search_results_table_data" tv-data) (VBox/setVgrow table-view Priority/ALWAYS) table-view-pane)) (defn create-search-pane [flow-id] (ui/v-box :childs [(ui/label :text (format "Flow: %d" flow-id)) (ui/v-box :childs [(create-search-params-pane flow-id) (create-results-table-pane flow-id)])])) (defn search-window [flow-id] (let [window-w 1000 window-h 600 scene (Scene. (create-search-pane flow-id) window-w window-h) stage (doto (Stage.) (.setTitle "FlowStorm search") (.setScene scene))] (.setOnCloseRequest stage (event-handler [_] (dbg-state/unregister-jfx-stage! stage))) (dbg-state/register-jfx-stage! stage) (let [{:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) window-w window-h)] (.setX stage x) (.setY stage y)) (-> stage .show))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/main.clj ================================================ (ns flow-storm.debugger.ui.main "Main UI sub-component which renders the GUI using JavaFX. Defines a pretty standard JavaFX application which uses `flow-storm.debugger.state` for mem state storage. The main entry point is `start-ui` and `stop-ui` can be used for stopping the component gracefully. One peculiarity of this JavaFX application is that it use a custom index (defined in `flow-storm.debugger.state`) for storing references to differet javafx Nodes instead of javafx own component ID system. Reference are stored and retrieved using `store-obj` and `obj-lookup` respectively. This namespace defines the outer window with the top bar and tools tabs. All tools screens are defined inside flow-storm.debugger.ui.*.screen.clj " (:require [flow-storm.debugger.ui.utils :as ui-utils :refer [event-handler key-combo-match?]] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.ui.flows.screen :as flows-screen] [flow-storm.debugger.ui.flows.general :as ui-general :refer [show-message]] [flow-storm.debugger.ui.browser.screen :as browser-screen] [flow-storm.debugger.ui.tasks :as tasks] [flow-storm.debugger.ui.outputs.screen :as outputs-screen] [flow-storm.debugger.ui.docs.screen :as docs-screen] [flow-storm.debugger.ui.flows.bookmarks :as bookmarks] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.state :as dbg-state :refer [obj-lookup store-obj]] [flow-storm.utils :as utils :refer [log log-error]] [flow-storm.state-management :refer [defstate]] [flow-storm.debugger.docs] [flow-storm.debugger.tutorials.basics :as tut-basics] [flow-storm.debugger.user-guide :as user-guide] [flow-storm.debugger.ui.plugins :as plugins] [clojure.java.io :as io] [clojure.string :as str]) (:import [com.jthemedetecor OsThemeDetector] [java.awt Taskbar Toolkit Taskbar$Feature] [javafx.scene Scene] [javafx.scene.image Image] [javafx.stage Stage] [javafx.application Platform] [javafx.scene.input KeyCode KeyEvent] [javafx.scene.control Button ProgressBar] [javafx.scene.layout HBox])) (declare start-ui) (declare stop-ui) (declare ui) (def ^:dynamic *killing-ui-from-window-close?* false) (defstate ui :start (fn [config] (start-ui config)) :stop (fn [] (stop-ui))) (defn clear-ui [] (ui-utils/run-later (outputs-screen/clear-outputs-ui) (doseq [fid (dbg-state/all-flows-ids)] (flows-screen/clear-debugger-flow fid)))) (defn bottom-box [] (let [progress-box (ui/h-box :childs []) ^ProgressBar heap-bar (ui/progress-bar :width 100) _ (.setProgress heap-bar 0) heap-max-lbl (ui/label :text "") heap-box (ui/h-box :childs [heap-bar heap-max-lbl]) repl-status-lbl (ui/label :text "REPL") runtime-status-lbl (ui/label :text "RUNTIME") box (ui/h-box :childs [progress-box repl-status-lbl runtime-status-lbl heap-box] :class "main-bottom-bar-box" :spacing 5 :align :center-right :pref-height 20) ] (store-obj "progress-box" progress-box) (store-obj "repl-status-lbl" repl-status-lbl) (store-obj "runtime-status-lbl" runtime-status-lbl) (store-obj "heap-bar" heap-bar) (store-obj "heap-max-lbl" heap-max-lbl) box)) (defn update-heap-indicator [{:keys [max-heap-bytes heap-size-bytes heap-free-bytes]}] (when (pos? max-heap-bytes) (ui-utils/run-later (let [[^ProgressBar heap-bar] (obj-lookup "heap-bar") [heap-max-lbl] (obj-lookup "heap-max-lbl") occupied-bytes (- heap-size-bytes heap-free-bytes) occ-perc (float (/ occupied-bytes max-heap-bytes)) max-gb (float (/ max-heap-bytes 1024 1024 1024))] (.setProgress heap-bar occ-perc) (ui-utils/set-text heap-max-lbl (format "%.2f Gb" max-gb)))))) (defn set-conn-status-lbl [lbl-key status] (ui-utils/run-later (try (let [[status-lbl] (obj-lookup (case lbl-key :ws "runtime-status-lbl" :repl "repl-status-lbl"))] (when status-lbl (ui-utils/clear-classes status-lbl) (ui-utils/add-class status-lbl "conn-status-lbl") (if status (ui-utils/add-class status-lbl "ok") (ui-utils/add-class status-lbl "fail")))) (catch Exception e (.printStackTrace e))))) (defn set-in-progress [in-progress?] (ui-utils/run-later (let [[^HBox box] (obj-lookup "progress-box")] (if in-progress? (let [prog-ind (ui/progress-indicator :size 20)] (-> box .getChildren .clear) (ui-utils/observable-add-all (.getChildren box) [prog-ind])) (-> box .getChildren ui-utils/observable-clear))))) (defn- main-tabs-pane [] (let [flows-tab (ui/tab :text "Flows" :class "vertical-tab" :content (flows-screen/main-pane) :id "tool-flows") browser-tab (ui/tab :text "Browser" :class "vertical-tab" :content (browser-screen/main-pane) :id "tool-browser") outputs-tab (ui/tab :text "Outputs" :class "vertical-tab" :content (outputs-screen/main-pane) :on-selection-changed (event-handler [_]) :id "tool-outputs") docs-tab (ui/tab :text "Docs" :class "vertical-tab" :content (docs-screen/main-pane) :on-selection-changed (event-handler [_]) :id "tool-docs") plugins-tabs (->> (plugins/plugins) (mapv (fn [p] (ui/tab :text (:plugin/label p) :class "vertical-tab" :content (ui/border-pane :center (:fx/node (plugins/create-plugin (:plugin/key p))) :class (name (:plugin/key p)) :paddings [10 10 10 10]) :id (:plugin/key p))))) tabs-p (ui/tab-pane :tabs (into [flows-tab browser-tab outputs-tab docs-tab] plugins-tabs) :rotate? true :closing-policy :unavailable :side :left :on-tab-change (fn [_ to-tab] (dbg-state/set-selected-tool (keyword (.getId to-tab))) (cond (= to-tab browser-tab) (browser-screen/get-all-namespaces) :else (let [p (some (fn [p] (when (= (.getId to-tab) (str (:plugin/key p))) p)) (plugins/plugins))] (when-let [{:keys [plugin/on-focus plugin/create-result]} p] (when on-focus (on-focus create-result))))))) _ (store-obj "main-tools-tab" tabs-p)] tabs-p)) (defn- toggle-debug-mode [] (dbg-state/toggle-debug-mode) (log (format "DEBUG MODE %s" (if (:debug-mode? (dbg-state/debugger-config)) "ENABLED" "DISABLED")))) (defn- ask-and-set-threads-limit [] (let [{:keys [text bool]} (ui/ask-text-and-bool-dialog :header "Set threads trace limit. FlowStorm will stop recording threads which hit the provided trace limit." :body "Limit :" :width 500 :height 100 :center-on-stage (dbg-state/main-jfx-stage) :bool-msg "Throw on limit?")] (when-not (str/blank? text) (runtime-api/set-thread-trace-limit rt-api {:limit (Integer/parseInt text) :break? bool})))) (defn- ask-and-set-heap-limit [] (let [{:keys [text bool]} (ui/ask-text-and-bool-dialog :header "Set heap limit. FlowStorm will stop recording when this heap limit in MBs is reached." :body "Limit :" :width 500 :height 100 :center-on-stage (dbg-state/main-jfx-stage) :bool-msg "Throw on limit?")] (when-not (str/blank? text) (runtime-api/set-heap-limit rt-api {:limit (Integer/parseInt text) :break? bool})))) (defn- goto-file-line [] (let [file-and-line (ui/ask-text-dialog :header "Goto file and line" :body ":" :width 800 :height 200 :center-on-stage (dbg-state/main-jfx-stage)) [file line] (when file-and-line (str/split file-and-line #":"))] (when file-and-line (tasks/submit-task runtime-api/find-expr-entry-task [{:file file :line (Integer/parseInt line)}] {:on-finished (fn [{:keys [result]}] (if result (let [{:keys [flow-id thread-id idx]} result] (flows-screen/goto-location {:flow-id flow-id :thread-id thread-id :idx idx})) (show-message (format "No recordings found for file %s at line %s" file line) :info)))})))) (defn- build-menu-bar [] (let [view-menu (ui/menu :label "_View" :items [{:text "Bookmarks" :on-click (fn [] (bookmarks/show-bookmarks))} {:text "Toggle theme" :on-click (fn [] (dbg-state/rotate-theme) (dbg-state/reset-theming)) :accel {:mods [:ctrl] :key-code KeyCode/T}} {:text "Increase font size" :on-click (fn [] (dbg-state/inc-font-size) (dbg-state/reset-theming)) :accel {:mods [:ctrl :shift] :key-code KeyCode/EQUALS}} {:text "Decrease font size" :on-click (fn [] (dbg-state/dec-font-size) (dbg-state/reset-theming)) :accel {:mods [:ctrl] :key-code KeyCode/MINUS}} {:text "Toggle log mode (for debugging FlowStorm)" :on-click (fn [] (toggle-debug-mode)) :accel {:mods [:ctrl] :key-code KeyCode/D}}]) actions-menu (ui/menu :label "_Actions" :items [{:text "Clear all" :on-click (fn [] ;; this will cause the runtime to fire back flow discarded events ;; that will get rid of the flows UI side of things (runtime-api/clear-runtime-state rt-api) (runtime-api/clear-api-cache rt-api) (outputs-screen/clear-outputs-ui))} {:text "Clear current tool" :on-click (fn [] (case (dbg-state/selected-tool) :tool-flows (flows-screen/discard-all-flows) :tool-browser nil :tool-outputs (outputs-screen/clear-outputs) :tool-docs nil ;; TODO: execute clear on the selected plugin ?? nil)) :accel {:mods [:ctrl] :key-code KeyCode/L}} {:text "Unblock all threads" :on-click (fn [] (runtime-api/unblock-all-threads rt-api)) :accel {:mods [:ctrl] :key-code KeyCode/U}} {:text "Goto file:line" :on-click (fn [] (goto-file-line))}]) config-menu (ui/menu :label "_Config" :items [{:text "Set threads limit" :on-click (fn [] (ask-and-set-threads-limit))} {:text "Set heap limit" :on-click (fn [] (ask-and-set-heap-limit))} {:text "Auto jump to exceptions" :check-item? true :checked? (:auto-jump-on-exception? (dbg-state/debugger-config)) :on-click (fn [enable?] (dbg-state/set-auto-jump-on-exception enable?))} {:text "Auto update UI" :check-item? true :checked? (:auto-update-ui? (dbg-state/debugger-config)) :on-click (fn [enable?] (dbg-state/set-auto-update-ui enable?))} {:text "Pretty print previews" :check-item? true :checked? (:pprint-previews? (dbg-state/debugger-config)) :on-click (fn [enable?] (dbg-state/set-pprint-previews enable?))} {:text "Call tree update" :check-item? true :checked? (:call-tree-update? (dbg-state/debugger-config)) :on-click (fn [enable?] (dbg-state/set-call-tree-update enable?))}]) help-menu (ui/menu :label "_Help" :items [{:text "Tutorial" :on-click (fn [] (if (dbg-state/clojure-storm-env?) (tut-basics/start-tutorials-ui) (show-message "This tutorial is not available in vanilla mode" :warning)))} {:text "User Guide" :on-click (fn [] (user-guide/show-user-guide))}])] (ui/menu-bar :menues [view-menu actions-menu config-menu help-menu]))) (defn- build-top-tool-bar-pane [] (let [task-cancel-btn (ui/icon-button :icon-name "mdi-playlist-remove" :tooltip "Cancel current running task (search, etc) (Ctrl-g)" :on-click (fn [] (runtime-api/interrupt-all-tasks rt-api)) :disable true) inst-toggle (ui/toggle-button {:label "Inst Enable" :on-change (fn [on?] (if (dbg-state/clojure-storm-env?) (runtime-api/turn-storm-instrumentation rt-api on?) (show-message "This functionality is only available in Storm modes" :warning)))}) tools [task-cancel-btn inst-toggle]] (store-obj "task-cancel-btn" task-cancel-btn) (store-obj "inst-toggle-btn" inst-toggle) (ui/toolbar :childs tools))) (defn set-instrumentation-ui [enable?] (let [[inst-toggle] (obj-lookup "inst-toggle-btn")] (.setSelected inst-toggle enable?))) (defn- build-top-bar-pane [] (ui/v-box :childs [(build-menu-bar) (build-top-tool-bar-pane)] :spacing 5)) (defn set-task-cancel-btn-enable [enable?] (ui-utils/run-later (let [[^Button task-cancel-btn] (obj-lookup "task-cancel-btn")] (.setDisable task-cancel-btn (not enable?)) (if enable? (ui-utils/add-class task-cancel-btn "attention") (ui-utils/rm-class task-cancel-btn "attention"))))) (defn- build-main-pane [] (ui/border-pane :top (build-top-bar-pane) :center (main-tabs-pane) :bottom (bottom-box) :class "main-pane")) (defn- start-theme-listener [on-theme-change] (try (let [detector (OsThemeDetector/getDetector) listener (reify java.util.function.Consumer (accept [_ dark?] (ui-utils/run-later (on-theme-change dark?))))] (log "Registering os theme-listener") (.registerListener detector listener) listener) (catch Exception e (log-error "Couldn't start theme listener" e)))) (defn stop-ui [] (let [{:keys [theme-listener]} ui] ;; remove the OS theme listener (when theme-listener (log "Removing os theme-listener") (.removeListener (OsThemeDetector/getDetector) theme-listener)) ;; close all stages (if *killing-ui-from-window-close?* ;; if we are comming from window-close close all the stages but ;; the first (the one being closed) in the javafx thread (doseq [stage (rest (dbg-state/jfx-stages))] (.close ^Stage stage)) ;; if we are not comming from a windows close, like a stop-system ;; then block until we close all stages on the javafx thread (ui-utils/run-now (doseq [stage (dbg-state/jfx-stages)] (.close ^Stage stage)))))) (defn create-flow [{:keys [flow-id timestamp]}] ;; lets clear the entire cache every time a flow gets created, just to be sure ;; we don't reuse old flows values on this flow (runtime-api/clear-api-cache rt-api) ;; make sure with discard any previous flow for the same id (flows-screen/clear-debugger-flow flow-id) (dbg-state/create-flow flow-id timestamp) (flows-screen/create-empty-flow flow-id) (ui-general/select-main-tools-tab "tool-flows") (flows-screen/update-threads-list flow-id)) (defn setup-ui-from-runtime-config "This function is meant to be called after all the system has started, to configure the part of UI that depends on runtime state." [] (ui-utils/run-later (when-let [{:keys [storm? recording? total-order-recording? flow-storm-nrepl-middleware?] :as runtime-config} (runtime-api/runtime-config rt-api)] (log (str "Runtime config retrieved :" runtime-config)) (let [all-flows-ids (->> (runtime-api/all-flows-threads rt-api) (map first) (into #{}))] (dbg-state/set-runtime-config runtime-config) (flows-screen/set-recording-btn recording?) (flows-screen/set-multi-timeline-recording-btn total-order-recording?) (when storm? (let [storm-prefixes (runtime-api/get-storm-instrumentation rt-api)] (browser-screen/enable-storm-controls) (browser-screen/update-storm-instrumentation storm-prefixes))) (when-not flow-storm-nrepl-middleware? (outputs-screen/set-middleware-not-available)) (doseq [fid all-flows-ids] (create-flow {:flow-id fid})))))) (defn setup-instrumentation-ui [] (ui-utils/run-later (when (dbg-state/clojure-storm-env?) (let [inst-enable? (runtime-api/storm-instrumentation-enable? rt-api)] (set-instrumentation-ui inst-enable?))))) (defn open-flow-threads-menu [flow-id] (flows-screen/select-flow-tab flow-id) (let [[{:keys [menu-button]}] (obj-lookup flow-id "flow_threads_menu")] (when menu-button (.show menu-button)))) (defn start-ui [config] (Platform/setImplicitExit false) (ui-utils/run-now (try (let [scene (Scene. (build-main-pane) 1024 768) stage (doto (Stage.) (.setTitle (or (:title config) "Flowstorm debugger")) (.setScene scene) (.setOnCloseRequest (event-handler [_] (binding [*killing-ui-from-window-close?* true] ((resolve 'flow-storm.debugger.main/stop-debugger)))))) theme-listener (when (= :auto (:theme config)) (start-theme-listener (fn [dark?] (dbg-state/set-theme (if dark? :dark :light)) (dbg-state/reset-theming))))] (dbg-state/register-jfx-stage! stage) (dbg-state/reset-theming) ;; set icon on application bar (.add (.getIcons stage) (Image. (io/input-stream (io/resource "flowstorm/icons/icon.png")))) ;; set icon on taskbar/dock (when (Taskbar/isTaskbarSupported) (let [taskbar (Taskbar/getTaskbar)] (when (.isSupported taskbar Taskbar$Feature/ICON_IMAGE) (.setIconImage taskbar (.getImage (Toolkit/getDefaultToolkit) (io/resource "flowstorm/icons/icon.png")))))) (doto scene (.setOnKeyPressed (event-handler [^KeyEvent kev] (let [key-name (.getName (.getCode kev))] (cond (key-combo-match? kev "g" [:ctrl]) (runtime-api/interrupt-all-tasks rt-api) (key-combo-match? kev "f" [:shift]) (ui-general/select-main-tools-tab "tool-flows") (key-combo-match? kev "b" [:shift]) (ui-general/select-main-tools-tab "tool-browser") (key-combo-match? kev "o" [:shift]) (ui-general/select-main-tools-tab "tool-outputs") (key-combo-match? kev "d" [:shift]) (ui-general/select-main-tools-tab "tool-docs") (= key-name "0") (open-flow-threads-menu 0) (= key-name "1") (open-flow-threads-menu 1) (= key-name "2") (open-flow-threads-menu 2) (= key-name "3") (open-flow-threads-menu 3) (= key-name "4") (open-flow-threads-menu 4) (= key-name "5") (open-flow-threads-menu 5) (= key-name "6") (open-flow-threads-menu 6) (= key-name "7") (open-flow-threads-menu 7) (= key-name "8") (open-flow-threads-menu 8) (= key-name "9") (open-flow-threads-menu 9))))) (.setRoot (build-main-pane))) (-> stage .show) {:theme-listener theme-listener}) (catch Exception e (log-error "UI Thread exception" e))))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/outputs/screen.clj ================================================ (ns flow-storm.debugger.ui.outputs.screen (:require [flow-storm.debugger.ui.utils :as ui-utils] [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.state :refer [store-obj obj-lookup] :as dbg-state] [flow-storm.debugger.ui.flows.screen :as flows-screen] [flow-storm.debugger.ui.tasks :as tasks] [flow-storm.utils :as utils] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]] [flow-storm.debugger.ui.flows.general :as ui-general] [flow-storm.debugger.ui.data-windows.data-windows :as data-windows]) (:import [javafx.scene.layout Priority VBox HBox] [javafx.scene.control ListView TextArea])) (defn- update-outputs-data-window [val-ref stack-key] (runtime-api/data-window-push-val-data rt-api :outputs val-ref {:dw-id :outputs :stack-key stack-key :root? true})) (defn update-last-evals [last-evals-refs] (let [[{:keys [add-all clear]}] (obj-lookup "last-vals-list-view-data") last-ref (last last-evals-refs)] (clear) (update-outputs-data-window last-ref "eval") (add-all last-evals-refs))) (defn add-out-write [msg] (let [[^TextArea out-and-err-txt] (obj-lookup "out-and-err-text-area")] (.appendText out-and-err-txt msg))) (defn add-err-write [msg] (let [[^TextArea out-and-err-txt] (obj-lookup "out-and-err-text-area")] (.appendText out-and-err-txt msg))) (defn add-tap-value [val-ref] (let [[{:keys [add-all ^ListView list-view]}] (obj-lookup "taps-list-view-data") lv-size (count (.getItems list-view))] (update-outputs-data-window val-ref "tap") (add-all [val-ref]) (.scrollTo list-view lv-size))) (defn clear-outputs-ui [] (ui-utils/run-later (let [[{:keys [clear]}] (obj-lookup "taps-list-view-data")] (clear)) (let [[out-and-error-txt] (obj-lookup "out-and-err-text-area")] (.setText out-and-error-txt "")) (let [[{:keys [clear]}] (obj-lookup "last-vals-list-view-data")] (clear)))) (defn set-middleware-not-available [] ;; This is kind of hacky but an easy way of letting the user know it ;; needs the middleware for this functionality (let [txt "This functionality is only available for Clojure and needs flow-storm nrepl middleware available." [out-and-error-txt] (obj-lookup "out-and-err-text-area") [last-vals-lv-data] (obj-lookup "last-vals-list-view-data")] (.setText out-and-error-txt txt) ((:add-all last-vals-lv-data) [(with-meta [] {:val-preview txt})]))) (defn find-and-jump-tap-val [vref] (tasks/submit-task runtime-api/find-expr-entry-task [{:from-idx 0 :identity-val vref}] {:on-finished (fn [{:keys [result]}] (when result (ui-general/select-main-tools-tab "tool-flows") (flows-screen/goto-location result)))})) (defn clear-outputs [] (runtime-api/clear-outputs rt-api) (clear-outputs-ui)) (defn main-pane [] (let [last-evals-lv-data (ui/list-view :editable? false :cell-factory (fn [list-cell val-ref] (let [val-list-text (-> val-ref meta :val-preview utils/remove-newlines)] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :text val-list-text))))) :on-click (fn [mev sel-items _] (let [val-ref (first sel-items)] (cond (ui-utils/mouse-primary? mev) (update-outputs-data-window val-ref "eval")))) :selection-mode :single) out-and-err-txt (ui/text-area :text "" :editable? false :class "monospaced") taps-lv-data (ui/list-view :editable? false :cell-factory (fn [list-cell val-ref] (let [val-list-text (-> val-ref meta :val-preview utils/remove-newlines)] (-> list-cell (ui-utils/set-text nil) (ui-utils/set-graphic (ui/label :text val-list-text))))) :on-click (fn [mev sel-items {:keys [list-view]}] (let [val-ref (first sel-items)] (cond (ui-utils/mouse-primary? mev) (update-outputs-data-window val-ref "tap") (ui-utils/mouse-secondary? mev) (let [ctx-menu (ui/context-menu :items [{:text "Search value on Flows" :on-click (fn [] (find-and-jump-tap-val val-ref))}])] (ui-utils/show-context-menu :menu ctx-menu :parent list-view :mouse-ev mev))))) :selection-mode :single) out-and-err-lv-pane (ui/v-box :childs [(ui/label :text "*out* and *err*") out-and-err-txt]) last-evals-lv (:list-view last-evals-lv-data) taps-lv (:list-view taps-lv-data) val-dw-pane (ui/v-box :childs [(data-windows/data-window-pane {:data-window-id :outputs})] :paddings [10 10 10 10] :class "outputs-dw") last-vals-box (ui/v-box :childs [(ui/label :text "Last evals") last-evals-lv] :spacing 5) taps-box (ui/v-box :childs [(ui/label :text "Taps") taps-lv] :spacing 5) last-vals-and-taps (ui/h-box :childs [last-vals-box taps-box]) split-pane (ui/split :orientation :vertical :childs [val-dw-pane last-vals-and-taps out-and-err-lv-pane] :sizes [0.4 0.3 0.3] ) clear-btn (ui/icon-button :icon-name "mdi-delete-forever" :tooltip "Clean all outputs (Ctrl-l)" :on-click (fn [] (clear-outputs))) controls (ui/h-box :childs [clear-btn] :paddings [10 10 10 10]) main-p (ui/border-pane :top controls :center split-pane)] (VBox/setVgrow out-and-err-txt Priority/ALWAYS) (VBox/setVgrow taps-lv Priority/ALWAYS) (HBox/setHgrow taps-lv Priority/ALWAYS) (VBox/setVgrow last-evals-lv Priority/ALWAYS) (HBox/setHgrow last-evals-lv Priority/ALWAYS) (HBox/setHgrow last-vals-and-taps Priority/ALWAYS) (HBox/setHgrow last-vals-box Priority/ALWAYS) (HBox/setHgrow taps-box Priority/ALWAYS) (VBox/setVgrow split-pane Priority/ALWAYS) (VBox/setVgrow main-p Priority/ALWAYS) (store-obj "taps-list-view-data" taps-lv-data) (store-obj "out-and-err-text-area" out-and-err-txt) (store-obj "last-vals-list-view-data" last-evals-lv-data) main-p)) (comment (add-tap-value {:a 1 :b 2}) (add-tap-value {:a 1 :b 3}) ) ================================================ FILE: src-dbg/flow_storm/debugger/ui/plugins.clj ================================================ (ns flow-storm.debugger.ui.plugins (:require [flow-storm.utils :as utils :refer [log-error log]])) (defonce *plugins (atom {})) (defn register-plugin [key {:keys [label on-create on-focus on-flow-clear css-resource dark-css-resource light-css-resource]}] (swap! *plugins assoc key {:plugin/key key :plugin/label label :plugin/on-create on-create :plugin/on-focus on-focus :plugin/on-flow-clear on-flow-clear :plugin/create-result nil :plugin/css-resource css-resource :plugin/dark-css-resource dark-css-resource :plugin/light-css-resource light-css-resource})) (defn create-plugin [plugin-key] (try (when-let [{:keys [plugin/on-create]} (get @*plugins plugin-key)] (let [create-res (on-create nil)] (swap! *plugins assoc-in [plugin-key :plugin/create-result] create-res) create-res)) (catch Exception e (log-error (str "Error creating plugin " plugin-key e ))))) (defn plugins [] (vals @*plugins)) (defn load-plugins-namespaces [{:keys [plugins-namespaces-set]}] (let [namespaces (mapv symbol plugins-namespaces-set)] (doseq [n namespaces] (log (format "Requiring plugin ns %s" n)) (require n)))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/tasks.clj ================================================ (ns flow-storm.debugger.ui.tasks (:require [flow-storm.debugger.events-queue :as events-queue] [flow-storm.debugger.ui.utils :refer [run-later]] [flow-storm.debugger.runtime-api :as runtime-api :refer [rt-api]])) (defn submit-task [rt-task-func rt-task-args {:keys [on-progress on-finished]}] (let [func-task-id (apply rt-task-func (into [rt-api] rt-task-args))] (events-queue/add-dispatch-fn func-task-id (fn [[ev-type {:keys [task-id] :as task-info}]] (when (= func-task-id task-id) (case ev-type :task-progress (run-later (when on-progress (on-progress task-info))) :task-finished (run-later (when on-finished (on-finished task-info)) (events-queue/rm-dispatch-fn func-task-id)) :task-failed (events-queue/rm-dispatch-fn func-task-id) nil)))) (runtime-api/start-task rt-api func-task-id))) ================================================ FILE: src-dbg/flow_storm/debugger/ui/utils.clj ================================================ (ns flow-storm.debugger.ui.utils "Mostly javaFx Utilities for building the UI" (:require [flow-storm.utils :as utils :refer [log-error]] [flow-storm.debugger.state :as dbg-state :refer [store-obj obj-lookup]]) (:import [javafx.scene.control ScrollPane ComboBox ListCell ButtonBase Labeled SelectionModel TabPane Tab CheckBox TextInputControl ContextMenu] [javafx.scene.input KeyCharacterCombination KeyCombination$Modifier KeyCombination MouseButton MouseEvent] [javafx.event Event] [javafx.scene.layout HBox Region Pane] [javafx.stage Screen Stage] [javafx.scene Node] [java.util.function Predicate] [org.kordamp.ikonli.javafx FontIcon] [javafx.collections FXCollections ObservableList] [javafx.geometry Insets Pos Rectangle2D] [com.jthemedetecor OsThemeDetector] [java.awt Toolkit] [java.awt.datatransfer StringSelection] [javafx.application Platform])) (defn init-toolkit [] (let [p (promise)] (try (Platform/startup (fn [] (deliver p true))) (catch Exception _ (deliver p false))) (if @p (utils/log "JavaFX toolkit initialized") (utils/log "JavaFX toolkit already initialized")))) (defn run-later* [f] (javafx.application.Platform/runLater f)) (defmacro run-later [& body] `(run-later* (fn ~(symbol "run-later-fn") [] (try ~@body (catch Exception e# (log-error (str "Exception in UI thread @1 " (.getMessage e#)) e#)))))) (defn run-now* [f] (let [result (promise)] (run-later (deliver result (try (f) (catch Exception e (log-error (str "Exception in UI thread @2" (.getMessage e)) e))))) @result)) (defmacro run-now [& body] `(run-now* (fn ~(symbol "run-now-fn") [] ~@body))) (defn event-handler* [f] (reify javafx.event.EventHandler (handle [_ e] (f e)))) (defmacro event-handler [arg & body] `(event-handler* (fn ~(symbol "event-handler-fn") ~arg ~@body))) (defn mod-k->key-comb [m] (case m :shift KeyCombination/SHIFT_DOWN :ctrl KeyCombination/CONTROL_DOWN)) (defn stage-screen-info [^Stage stage] (let [^Screen screen (first (Screen/getScreensForRectangle (.getX stage) (.getY stage) (.getWidth stage) (.getHeight stage))) ^Rectangle2D bounds (.getBounds screen) screen-width (.getWidth bounds) screen-height (.getHeight bounds)] {:screen-width screen-width :screen-height screen-height :screen-visual-center-x (+ (/ screen-width 2) (.getMinX bounds)) :screen-visual-center-y (+ (/ screen-height 2) (.getMinY bounds))})) (defn stage-center-box [^Stage reference-stg target-w target-h] (let [ref-x (.getX reference-stg) ref-y (.getY reference-stg) ref-w (.getWidth reference-stg) ref-h (.getHeight reference-stg) ref-center-x (+ ref-x (/ ref-w 2)) ref-center-y (+ ref-y (/ ref-h 2)) tgt-x (- ref-center-x (/ target-w 2)) tgt-y (- ref-center-y (/ target-h 2))] {:x tgt-x :y tgt-y})) (defn ensure-node-visible-in-scroll-pane [^ScrollPane scroll-pane ^Node node y-perc] (let [scroll-pane-content (.getContent scroll-pane) ;; first take the top left corner of the `node` and the `scroll-pane` into scene coordinates ;; we are using the `scroll-pane` top left corner as the view-port top left corner scene-node-bounds (.localToScene node (.getBoundsInLocal node)) scene-scroll-pane-bounds (.localToScene scroll-pane (.getBoundsInLocal scroll-pane)) ;; now transform those related to the content pane, so we have everything in content coordinates ;; and can make calculations node-bounds-in-content (.sceneToLocal scroll-pane-content scene-node-bounds) viewport-bounds-in-content (.sceneToLocal scroll-pane-content scene-scroll-pane-bounds) pane-w (-> scroll-pane .getContent .getBoundsInLocal .getWidth) pane-h (-> scroll-pane .getContent .getBoundsInLocal .getHeight) node-interesting-x-in-content (.getMinX node-bounds-in-content) node-interesting-y-in-content (+ (.getMinY node-bounds-in-content) (* y-perc (- (.getMaxY node-bounds-in-content) (.getMinY node-bounds-in-content)))) pane-view-min-x (.getMinX viewport-bounds-in-content) pane-view-max-x (+ pane-view-min-x (-> scroll-pane .getViewportBounds .getWidth)) pane-view-min-y (.getMinY viewport-bounds-in-content) pane-view-max-y (+ pane-view-min-y (-> scroll-pane .getViewportBounds .getHeight)) ;; check if the node is visible in both axis node-visible-x? (<= pane-view-min-x node-interesting-x-in-content pane-view-max-x) node-visible-y? (<= pane-view-min-y node-interesting-y-in-content pane-view-max-y)] ;; if the node isn't visible in any of the axis scroll accordingly (when-not node-visible-x? (.setHvalue scroll-pane (/ node-interesting-x-in-content pane-w))) (when-not node-visible-y? (.setVvalue scroll-pane (/ node-interesting-y-in-content pane-h))))) (defn list-cell-factory [update-item-fn] (proxy [ListCell] [] (updateItem [item empty?] (proxy-super updateItem item empty?) (if empty? (do (.setText ^ListCell this nil) (.setGraphic ^ListCell this nil)) (update-item-fn ^ListCell this item))))) (defn add-class [^Node node class] (.add (.getStyleClass node) class)) (defn rm-class [^Node node class] (.removeIf (.getStyleClass node) (proxy [Predicate] [] (test [c] (= c class))))) (defn clear-classes [^Node node] (.clear (.getStyleClass node))) (defn update-button-icon [btn icon-name] (doto btn (.setGraphic (if (string? icon-name) (FontIcon. ^String icon-name) (HBox. (into-array Node (mapv (fn [in] (FontIcon. ^String in)) icon-name))))))) (defn observable-add-all [^ObservableList olist coll] (.addAll olist ^objects (into-array Object coll))) (defn observable-clear [^ObservableList olist] (.clear olist)) (defn set-disable [^Node node x] (.setDisable node x)) (defn set-min-size-wrap-content [^Region node] (.setMinHeight node (Region/USE_PREF_SIZE)) node) (defn show-context-menu [& {:keys [^ContextMenu menu ^Node parent x y mouse-ev]}] (let [[^ContextMenu curr-menu] (obj-lookup "current_context_menu") ^double x (or x (when mouse-ev (.getScreenX mouse-ev)) 0) ^double y (or y (when mouse-ev (.getScreenY mouse-ev)) 0)] (when curr-menu (.hide curr-menu)) (.show menu parent x y) (store-obj "current_context_menu" menu))) (defn remove-newlines [s] (-> ^String s (.replaceAll "\\n" "") (.replaceAll "\\r" ""))) (defn get-current-os-theme [] (try (if (.isDark (OsThemeDetector/getDetector)) :dark :light) (catch Exception e (log-error "Couldn't retrieve os theme, setting :light by default" e) :light))) (defn set-clipboard [text] (let [str-sel (StringSelection. text)] (-> (Toolkit/getDefaultToolkit) .getSystemClipboard (.setContents str-sel str-sel)))) (defn copy-selected-frame-to-clipboard ([fn-ns fn-name] (copy-selected-frame-to-clipboard fn-ns fn-name nil)) ([fn-ns fn-name args-vec] (let [fqfn (if fn-ns (format "%s/%s" fn-ns fn-name) fn-name) clip-text (if args-vec (format "(apply %s (flow-storm.runtime.values/deref-val-id %d))" fqfn (:vid args-vec)) fqfn)] (set-clipboard clip-text)))) (defn key-combo-match? "Return true if the keyboard event `kev` matches the `key-name` and `modifiers`. `key-name` should be a stirng with the key name. `modifiers` should be a collection of modifiers like :ctrl, :shift" [kev key-name modifiers] (let [k (KeyCharacterCombination. key-name (into-array KeyCombination$Modifier (mapv mod-k->key-comb modifiers)))] (.match k kev))) (defn add-childrens-to-pane [^Pane pane childs] (observable-add-all (.getChildren pane) childs)) (defn set-button-action [^ButtonBase button f] (.setOnAction button (event-handler [_] (f)))) (defn set-text [^Labeled labeled ^String text] (.setText labeled text) labeled) (defn set-text-input-text [^TextInputControl tic ^String text] (.setText tic text) tic) (defn set-graphic [^Labeled labeled ^Node node] (.setGraphic labeled node) labeled) (defn set-padding ([^Region region pad] (.setPadding region (Insets. pad))) ([^Region region pad-top pad-right pad-bottom pad-left] (.setPadding region (Insets. pad-top pad-right pad-bottom pad-left)))) (defn consume [^Event e] (.consume e)) (defn alignment [k] (case k :baseline-center Pos/BASELINE_CENTER :baseline-left Pos/BASELINE_LEFT :baseline-right Pos/BASELINE_RIGHT :bottom-center Pos/BOTTOM_CENTER :bottom-left Pos/BOTTOM_LEFT :bottom-right Pos/BOTTOM_RIGHT :center Pos/CENTER :center-left Pos/CENTER_LEFT :center-right Pos/CENTER_RIGHT :top-center Pos/TOP_CENTER :top-left Pos/TOP_LEFT :top-right Pos/TOP_RIGHT)) (defn mouse-primary? [^MouseEvent mev] (= MouseButton/PRIMARY (.getButton mev))) (defn mouse-secondary? [^MouseEvent mev] (= MouseButton/SECONDARY (.getButton mev))) (defn double-click? [^MouseEvent mev] (= 2 (.getClickCount mev))) (defn selection-select-idx [^SelectionModel model idx] (.select model ^int idx)) (defn selection-select-obj [^SelectionModel model obj] (.select model obj)) (defn selection-select-first [^SelectionModel model] (.selectFirst model)) (defn combo-box-set-items [^ComboBox cbox items] (let [observable-list (FXCollections/observableArrayList)] (.clear observable-list) (.setItems cbox observable-list) (observable-add-all observable-list items))) (defn combo-box-set-selected [^ComboBox cbox item] (selection-select-obj (.getSelectionModel cbox) item)) (defn combo-box-get-selected-item [^ComboBox cbox] (.getSelectedItem (.getSelectionModel cbox))) (defn add-tab-pane-tab [^TabPane tp ^Tab t] (observable-add-all (.getTabs tp) [t])) (defn rm-tab-pane-tab [^TabPane tp ^Tab t] (-> tp .getTabs (.remove t))) (defn checkbox-checked? [^CheckBox cb] (.isSelected cb)) (defn pane-children [^Pane p] (.getChildren p)) (defn add-change-listener []) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Node index ids builders ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn form-token-id [form-id coord] (format "form_token_%d_%s" form-id (hash coord))) (defn flow-tab-id [flow-id] (format "flow_tab_%d" flow-id)) (defn thread-form-box-id [form-id] (format "form_box_%d" form-id)) (defn thread-form-paint-fn [form-id] (format "form_paint_fn_%d" form-id)) (defn thread-pprint-area-id [pane-id] (format "pprint_area_%s" pane-id)) (defn thread-pprint-type-lbl-id [pane-id] (format "pprint_type_lbl_%s" pane-id)) (defn thread-pprint-extra-lbl-id [pane-id] (format "pprint_extra_lbl_%s" pane-id)) (defn thread-pprint-def-btn-id [pane-id] (format "pprint_def_btn_id_%s" pane-id)) (defn thread-pprint-inspect-btn-id [pane-id] (format "pprint_inspect_btn_id_%s" pane-id)) (defn thread-pprint-tap-btn-id [pane-id] (format "pprint_tap_btn_id_%s" pane-id)) (defn thread-pprint-level-txt-id [pane-id] (format "pprint_level_txt_id_%s" pane-id)) (defn thread-pprint-meta-chk-id [pane-id] (format "pprint_meta_chk_id_%s" pane-id)) (defn thread-callstack-tree-cell [idx] (format "callstack_tree_cell_%d" idx)) ================================================ FILE: src-dbg/flow_storm/debugger/user_guide.clj ================================================ (ns flow-storm.debugger.user-guide (:require [flow-storm.debugger.ui.components :as ui] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.ui.utils :as ui-utils])) (defn show-user-guide [] (let [window-w 1200 window-h 800 {:keys [x y]} (ui-utils/stage-center-box (dbg-state/main-jfx-stage) window-w window-h) {:keys [web-view load-url]} (ui/web-view)] (load-url "https://flow-storm.github.io/flow-storm-debugger/user_guide.html") (ui/stage :scene (ui/scene :root web-view :window-width window-w :window-height window-h) :title "FlowStorm basics tutorial" :x x :y y :show? true))) ================================================ FILE: src-dbg/flow_storm/debugger/websocket.clj ================================================ (ns flow-storm.debugger.websocket "Component that manages the websocket server started by the debugger. It will : - automatically dispatch all events received from the runtime through its configured `on-ws-event` - provide `sync-remote-api-request` and `async-remote-api-request` to call the runtime." (:require [flow-storm.utils :refer [log log-error]] [flow-storm.json-serializer :as serializer] [flow-storm.state-management :refer [defstate]] [flow-storm.debugger.state :as dbg-state] [flow-storm.debugger.events-queue]) (:import [org.java_websocket.server WebSocketServer] [org.java_websocket.handshake ClientHandshake] [org.java_websocket WebSocket] [org.java_websocket.exceptions WebsocketNotConnectedException] [org.java_websocket.framing CloseFrame] [java.net InetSocketAddress] [java.util UUID])) (declare start-websocket-server) (declare stop-websocket-server) (declare websocket-server) (defstate websocket-server :start (fn [config] (start-websocket-server config)) :stop (fn [] (stop-websocket-server))) (defn async-remote-api-request "Call a runtime `method` asynchronously through the websocket with the provided `args`. `callback` will be called on response with the result object. `method` is a keyword with the method id as defined in `flow-storm.runtime.debuggers-api/api-fn` " [method args callback] (let [conn @(:remote-connection websocket-server) {:keys [ws-ready?]} (dbg-state/connection-status)] (if (or (not ws-ready?) (nil? conn)) (do (log-error "Skipping api-request because ws connection isn't ready.") (callback nil)) (try (let [request-id (str (UUID/randomUUID)) packet-str (serializer/serialize [:api-request request-id method args])] (.send ^WebSocket conn ^String packet-str) (swap! (:pending-commands-callbacks websocket-server) assoc request-id callback)) (catch WebsocketNotConnectedException wnce (log-error "Can't execute async api request because websocket isn't connected" wnce)) (catch Exception e (log-error "Error sending async command" e) nil))))) (defn sync-remote-api-request "Call a runtime `method` through the websocket with the provided `args`. Will block until we have the response. `method` is a keyword with the method id as defined in `flow-storm.runtime.debuggers-api/api-fn` `timeout` can be provided so we don't block indefinetly. " ([method args] (sync-remote-api-request method args 10000)) ([method args timeout] (let [p (promise)] (async-remote-api-request method args (fn [resp] (deliver p resp))) (let [v (deref p timeout :flow-storm/timeout)] (if (= v :flow-storm/timeout) (do (log-error (str "Timeout waiting for sync-remote-api-request response to " method " with " args)) nil) v))))) (defn process-remote-api-response [[request-id err-msg resp :as packet]] (let [callback (get @(:pending-commands-callbacks websocket-server) request-id)] (if err-msg (do (log-error (format "Error on process-remote-api-response : %s" packet)) ;; TODO: we should report errors to callers (callback nil)) (callback resp)))) (defn- create-ws-server [{:keys [port on-message on-open on-close on-start]}] (let [server (proxy [WebSocketServer] [(InetSocketAddress. port)] (onStart [] (log (format "WebSocket server started, listening on %s" port)) (on-start)) (onOpen [^WebSocket conn ^ClientHandshake handshake-data] (log (format "Got a connection %s" conn)) (when on-open (on-open conn))) (onMessage [conn message] (on-message conn message)) (onClose [conn code reason remote?] (log (format "Connection with debugger closed. conn=%s code=%s reson=%s remote?=%s" conn code reason remote?)) (on-close code reason remote?)) (onError [conn ^Exception e] (log-error "WebSocket error" e)))] server)) (defn stop-websocket-server [] (when-let [wss (:ws-server websocket-server)] (.stop wss)) (when-let [conn @(:remote-connection websocket-server)] (.close conn)) nil) (defn start-websocket-server [{:keys [on-ws-event on-ws-up on-ws-down]}] (let [{:keys [debugger-ws-port]} (dbg-state/debugger-config) remote-connection (atom nil) ws-ready (promise) ws-server (create-ws-server {:port debugger-ws-port :on-start (fn [] (deliver ws-ready true)) :on-open (fn [conn] (reset! remote-connection conn) (when on-ws-up (on-ws-up conn))) :on-message (fn [_ msg] (try (let [[msg-kind msg-body] (serializer/deserialize msg)] (case msg-kind :event (on-ws-event msg-body) :api-response (process-remote-api-response msg-body))) (catch Exception e (log-error (format "Error processing remote message '%s', error msg %s" msg (.getMessage e)))))) :on-close (fn [code _ _] (log-error (format "Connection closed with code %s" code)) (cond (or (= code CloseFrame/GOING_AWAY) (= code CloseFrame/ABNORMAL_CLOSE)) (when on-ws-down (on-ws-down)) :else nil) )})] ;; see https://github.com/TooTallNate/Java-WebSocket/wiki/Enable-SO_REUSEADDR ;; if we don't have this we get Address already in use when starting twice in a row (.setReuseAddr ws-server true) (.start ws-server) ;; wait for the websocket to be ready before finishing this subsystem start ;; just to avoid weird race conditions @ws-ready {:ws-server ws-server :pending-commands-callbacks (atom {}) :remote-connection remote-connection})) ================================================ FILE: src-dev/dev.clj ================================================ (ns dev "A bunch of utilities to help with development. After loading this ns you can : - `start-local` to start the UI and runtime - `stop` for gracefully stopping the system - `refresh` to make tools.namespace unmap and reload all the modified files" (:require [flow-storm.debugger.ui.main :as ui-main] [flow-storm.debugger.main :as main] [flow-storm.debugger.state :as dbg-state] [hansel.api :as hansel] [flow-storm.api :as fs-api] [flow-storm.runtime.indexes.api :as index-api] [flow-storm.runtime.indexes.timeline-index :as timeline-index] [flow-storm.tracer :as tracer] [flow-storm.utils :refer [log-error log] :as utils] [flow-storm.debugger.ui.utils :as ui-utils] [clj-reload.core :as reload] [flow-storm.form-pprinter :as form-pprinter] [dev-tester] [flow-storm.utils :as utils] [clojure.java.io :as io] [clojure.string :as str] [clojure.spec.alpha :as s] [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.debugger.ui.components :as ui] [flow-storm.runtime.values :as rt-values :refer [ScopeFrameP ScopeFrameSampleP]])) (set! *warn-on-reflection* true) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Utilities for interactive development ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- spec-instrument-state [] (add-watch dbg-state/state :spec-validator (fn [_ _ _ s] (when-not (s/valid? ::dbg-state/state s) (s/explain ::dbg-state/state s)))) nil) (defn- spec-uninstrument-state [] (remove-watch dbg-state/state :spec-validator)) (defn start-local [] (fs-api/local-connect {}) (spec-instrument-state)) (defn start-shadow-remote [port build-id] (main/start-debugger {:port port :repl-type :shadow :build-id build-id}) (spec-instrument-state)) (defn stop [] (fs-api/stop)) (defn refresh [] (let [running? (boolean dbg-state/state)] (log "Reloading system ...") (when running? (log "System is running, stopping it first ...") (fs-api/stop)) (reload/reload) (log "Reload done"))) (defn run-tester-1 [] (dev-tester/run)) (defn run-tester-2 [] (dev-tester/run-parallel)) ;;;;;;;;;;;;;;;;;;;;;;; ;; Vanilla FlowStorm ;; ;;;;;;;;;;;;;;;;;;;;;;; (comment (fs-api/instrument-namespaces-clj #{"dev-tester"} {:disable #{} #_#{:expr-exec :anonymous-fn :bind}}) (fs-api/uninstrument-namespaces-clj #{"dev-tester"}) #rtrace (dev-tester/boo [2 "hello" 6]) ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Querying indexes programatically ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (comment (run-tester-1) (run-tester-2) ((requiring-resolve 'dev-tester-12/run)) (def tl (index-api/get-timeline 24)) ;; set the thread-id (->> tl (take 10) (map index-api/as-immutable)) (count tl) (take 10 tl) (time (reduce (fn [r e] (inc r)) 0 tl)) (get tl 0) (nth tl 0) (empty? tl) (def total-timeline (index-api/total-order-timeline 0)) (->> total-timeline (take 10) (map index-api/as-immutable)) (index-api/find-fn-call-entry {:backward? true :fn-name "factorial"}) ;; Synthesizing all the spec information for parameters that flow into a function (defn fn-signatures [flow-id thread-id fn-ns fn-name] (let [frames (index-api/find-fn-frames flow-id thread-id fn-ns fn-name nil) signature-types (->> frames (reduce (fn [coll-samples frame] (conj coll-samples (mapv type (:args-vec frame)))) #{}))] signature-types)) (fn-signatures 0 24 "dev-tester" "factorial") (fn-signatures 0 24 "dev-tester" "other-function") ;; Find all the sub expressions at the same code coordinate and fn frame ;; than the one which evaluated at idx (defn frame-same-coord-values [flow-id thread-id idx] (let [{:keys [fn-call-idx coord]} (index-api/timeline-entry flow-id thread-id idx :at) {:keys [expr-executions]} (index-api/frame-data flow-id thread-id fn-call-idx {:include-exprs? true})] (->> expr-executions (reduce (fn [coll-vals expr-exec] (if (= coord (:coord expr-exec)) (conj coll-vals (:result expr-exec)) coll-vals)) [])))) (frame-same-coord-values 0 24 49) ;; sum on dev/run-tester-1 ;; Create a small debugger for the repl ;; ------------------------------------------------------------------------------------------- (def idx (atom 0)) (def flow-id 0) (def thread-id 24) (defn show-current [] (let [{:keys [type fn-ns fn-name coord fn-call-idx result] :as idx-entry} (index-api/timeline-entry flow-id thread-id @idx :at) {:keys [form-id]} (index-api/frame-data flow-id thread-id fn-call-idx {}) {:keys [form/form]} (index-api/get-form form-id)] (case type :fn-call (let [{:keys [fn-name fn-ns]} idx-entry] (println "Called" fn-ns fn-name)) (:expr :fn-return) (let [{:keys [coord result]} idx-entry] (form-pprinter/pprint-form-hl-coord form coord) (println "\n") (println "==[VAL]==>" (utils/colored-string result :yellow)))))) (defn step-next [] (swap! idx inc) (show-current)) (defn step-prev [] (swap! idx dec) (show-current)) (step-next) (step-prev)) ;;;;;;;;;;;;;;;;;;;;;;;;; ;; DataWindows testing ;; ;;;;;;;;;;;;;;;;;;;;;;;;; (defn make-sample-frame [from-x step] (let [[final-x samples] (loop [i 0 x from-x samples (transient [])] (if (< i 10000) (let [samp (reify ScopeFrameSampleP (sample-chan-1 [_] (+ (Math/sin x) 0)) (sample-chan-2 [_] (+ (Math/cos x) 0)))] (recur (inc i) (+ x step) (conj! samples samp))) [x (persistent! samples)]))] [final-x (reify ScopeFrameP (frame-samp-rate [_] 200e3) (frame-samples [_] samples))])) (defn scope-test [& _] (fs-api/local-connect {}) (let [dw-id :scope0 _ (fs-api/data-window-push-val dw-id (reify ScopeFrameP (frame-samp-rate [_] 1) (frame-samples [_] []))) x-step 0.001] (def th (doto (Thread. (fn [] (loop [from-x 0.0] (when-not (Thread/interrupted) (let [[new-x frame] (make-sample-frame from-x x-step)] (fs-api/data-window-val-update dw-id frame) (Thread/sleep 10) (recur (double new-x))))))) (.start))) ) ) (comment (require '[clj-async-profiler.core :as prof]) (prof/serve-ui 8081) (def test-frame (second (make-sample-frame 0 0.001))) (def s (first (rt-values/frame-samples test-frame))) (rt-values/sample-chan-1 s) (rt-values/sample-chan-2 s) (tap> test-frame) (tap> {:a (range)}) (tap> {:a {:name {:other :hello :bla "world"}} :b {:age 10}}) (-> (rt-values/frame-samples fr) second rt-values/sample-chan-1) (.interrupt th) ) ;;;;;;;;;;;;;;;;;;;;; ;; Other utilities ;; ;;;;;;;;;;;;;;;;;;;;; (comment (add-tap (bound-fn* println)) (Thread/setDefaultUncaughtExceptionHandler (reify Thread$UncaughtExceptionHandler (uncaughtException [_ _ throwable] (tap> throwable) (log-error "Unhandled exception" throwable)))) ) ================================================ FILE: src-dev/dev_tester.clj ================================================ (ns dev-tester) ;;;;;;;;;;;;;;;;;;;;;;; ;; Some testing code ;; ;;;;;;;;;;;;;;;;;;;;;;; (defn uncatched-throw [] (let [a (+ 1 2)] (/ a 0) (+ a 3))) (defn throw-forwarder [] (uncatched-throw)) (defn catcher [] (try (throw-forwarder) (catch Exception _ 45))) (defmacro dummy-sum-macro [a b] `(+ ~a ~b)) (defn factorial [n] (if (zero? n) 1 (* n (factorial (dec n))))) (defmulti do-it type) (defmethod do-it java.lang.Long [l] (factorial l)) (defmethod do-it java.lang.String [s] (count s)) (defprotocol Adder (add [x])) (defrecord ARecord [n] Adder (add [_] (+ n 1000))) (deftype AType [^int n] Adder (add [_] (int (+ n 42)))) (defprotocol Suber (sub [x])) (extend-protocol Adder java.lang.Long (add [l] (+ l 5))) (extend-type java.lang.Long Suber (sub [l] (- l 42))) (def other-function (fn [a b] (+ a b 10))) (defn inc-atom [a] (swap! a inc)) (defn hinted [a ^long b c ^long d] (+ a c (+ b d))) (defn lorem-ipsum [arg1 arg2 arg3] (str "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " "Proin vehicula euismod ligula, eu consectetur tortor facilisis vel." "Pellentesque " arg1 " elit nec, " arg2 " sagittis turpis. " "Duis fermentum mi et eros vehicula, id fringilla justo tincidunt." "Integer " arg3 " ut justo in dignissim. " "Proin ac ex eu sem sollicitudin hendrerit.")) (defn generate-lorem-ipsum [] (let [long-arg1 (apply str (repeat 120 "a")) long-arg2 (apply str (repeat 120 "b")) long-arg3 (apply str (repeat 120 "c"))] (lorem-ipsum long-arg1 long-arg2 long-arg3))) (defn make-a-case [x] (let [r (case x :type-a (+ 2 2) :type-b (+ 4 4))] r)) (defn boo [xs] (let [a 25 yy (other-function 4 5) hh (range) dly (delay (+ 1 2 (* 3 4))) *a (atom 10) _ (inc-atom *a) xx @*a b (dummy-sum-macro a 4) _a-binding (+ 1 2) -a-binding (+ 3 4) m ^{:meta1 true :meta2 "nice-meta-value"} {:a 5 :b ^:interesting-vector [1 2 3]} mm (assoc m :c 10) c (+ a b 7 @dly) d (add (->ARecord 5)) e (add (AType. 10)) j (loop [i 100 sum 0] (if (> i 0) (recur (dec i) (+ sum i)) sum)) z (catcher) li (generate-lorem-ipsum) w (make-a-case :type-a) ww (true? (instance? String "hello"))] (+ 1 2 3) (debugger [1 2 3 4]) (debugger (+ 1 2)) (debugger "debugger") (bookmark) (bookmark "bookmark") (->> xs (map (fn [x] (+ 1 (do-it x)))) (reduce + ) add sub (+ c d j e (hinted a c d j))))) (defn run [] (boo [1 "hello" 4])) (defn run-parallel [] (->> (range 4) (pmap (fn [i] (factorial i))) (reduce +))) ================================================ FILE: src-dev/dev_tester.cljs ================================================ (ns dev-tester (:require-macros [dev-tester :refer [dummy-sum-macro]])) (defn factorial [n] (if (zero? n) 1 (* n (factorial (dec n))))) (defn multi-arity ([a] (multi-arity a 10)) ([a b] (+ a b))) (defmulti do-it #(.-name (type %))) (defmethod do-it "Number" [l] (factorial l)) (defmethod do-it "String" [s] (count s)) (defprotocol Adder (add [x])) (defprotocol Suber (sub [x])) (defrecord ARecord [n] Adder (add [_] (+ n 1000))) (extend-protocol Adder number (add [l] (+ l 5))) (extend-type number Suber (sub [l] (- l 42))) (extend-type ARecord Suber (sub [r] (+ 42 (* 2 32)))) (defn boo [xs] (let [a (dummy-sum-macro 25 8) b (multi-arity a) c (+ a b 7) d (add (->ARecord 5)) j (loop [i 100 sum 0] (if (> i 0) (recur (dec i) (+ sum i)) sum))] (->> xs (map (fn [x] (+ 1 (do-it x)))) (reduce + ) add sub (+ c d j)))) (defn -main [& args] (js/console.log (boo [2 "hello" 8]))) ================================================ FILE: src-dev/dev_tester_12.clj ================================================ (ns dev-tester-12) (defn method-values [] (let [parser ^[String] Integer/parseInt parsed (mapv parser ["1" "2" "3"]) s (^[byte/1] String/new (byte-array [64 64])) i (Long/parseLong "123")] (+ (reduce + parsed) (count s) i))) (defn instance-methods [] (let [strs ["a" "b" "c"]] (mapv String/.toUpperCase strs))) (defn functional-interfaces [] ;; converts even? to Predicate (let [o (.removeIf (java.util.ArrayList. [1 2 3]) even?) ;; pull up to let binding ^java.util.function.Predicate p (fn [n] (even? n))] (.removeIf (java.util.ArrayList. [1 2 3]) p) ;; converts inc to UnaryOperator, uses new stream-seq! (->> (java.util.stream.Stream/iterate 1 inc) stream-seq! (take 10) doall) (mapv str (java.nio.file.Files/newDirectoryStream (.toPath (java.io.File. ".")) #(-> ^java.nio.file.Path % .toFile .isDirectory))))) (defn run [] (+ (method-values) (count (instance-methods)) (functional-interfaces) 42)) ================================================ FILE: src-dev/logging.properties ================================================ .level=SEVERE # Increase logging level for javafx to suppress warnings javafx.level=SEVERE ================================================ FILE: src-dev/user.clj ================================================ ================================================ FILE: src-inst/data_readers.clj ================================================ {trace flow-storm.api/read-trace-tag ctrace flow-storm.api/read-ctrace-tag rtrace flow-storm.api/read-rtrace-tag tap flow-storm.api/read-tap-tag tap-stack-trace flow-storm.api/read-tap-stack-trace-tag flow-storm.types/value-ref flow-storm.types/make-value-ref} ================================================ FILE: src-inst/flow_storm/api.clj ================================================ (ns flow-storm.api "API intended for users. Provides functionality to start the debugger and instrument forms." (:require [flow-storm.tracer :as tracer] [flow-storm.utils :refer [log] :as utils] [flow-storm.ns-reload-utils :as reload-utils] [hansel.api :as hansel] [hansel.instrument.utils :as inst-utils] [flow-storm.runtime.debuggers-api :as dbg-api] [flow-storm.runtime.events :as rt-events] [flow-storm.runtime.values :as rt-values] [clojure.string :as str] [clojure.stacktrace :as stacktrace])) ;; TODO: build script ;; Maybe we can figure out this ns names by scanning (all-ns) so ;; we don't need to list them here ;; Also maybe just finding main is enough, we can add to it a fn ;; that returns the rest of the functions we need (def debugger-main-ns 'flow-storm.debugger.main) (defn start-debugger-ui "Start the debugger UI when available on the classpath. Returns true when available, false otherwise." [config] (if-let [start-debugger (requiring-resolve (symbol (name debugger-main-ns) "start-debugger"))] (do (start-debugger config) true) (do (log "It looks like the debugger UI isn't present on the classpath.") false))) (defn stop-debugger-ui "Stop the debugger UI if it has been started." [] (if-let [stop-debugger (requiring-resolve (symbol (name debugger-main-ns) "stop-debugger"))] (stop-debugger) (log "It looks like the debugger UI isn't present on the classpath."))) (defn stop "Stop the flow-storm runtime part gracefully. If working in local mode will also stop the UI." [] (rt-events/clear-dispatch-fn!) ;; if we are running in local mode and running a debugger stop it (stop-debugger-ui) (dbg-api/stop-runtime) (log "System fully stopped")) (defn local-connect "Start a debugger under this same JVM process and connect to it. This is the recommended way of using the debugger for debugging code that generates a lot of data since data doesn't need to serialize/deserialize it like in a remote debugging session case. `config` should be a map containing : - `:verbose?` to log more stuff for debugging the debugger - `:theme` can be one of `:light`, `:dark` or `:auto` - `:styles` a string path to a css file if you want to override some started debugger styles Use `flow-storm.api/stop` to shutdown the system nicely." ([] (local-connect {})) ([config] (let [enqueue-event! (requiring-resolve 'flow-storm.debugger.events-queue/enqueue-event!) config (assoc config :local? true)] (dbg-api/start-runtime) (start-debugger-ui config) (rt-events/set-dispatch-fn enqueue-event!)))) (def jump-to-last-expression dbg-api/jump-to-last-expression-in-this-thread) (defn set-thread-trace-limit "Set a trace limit to all threads. When the limit is positive, if any thread timeline goes beyond the limit the thread code will throw an exception." [limit] (dbg-api/set-thread-trace-limit limit)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Clojure instrumentation ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn instrument-var-clj "Instruments any var. Lets say you are interested in debugging clojure.core/interpose you can do : (instrument-var-clj clojure.core/interpose) Then you can call : (interpose :a [1 2 3]) and it will show up in the debugger. Be careful instrumenting clojure.core functions or any functions that are being used by repl system code since can be called constantly and generate a lot of noise. Use `uninstrument-var-clj` to remove instrumentation. `opts` is a map that support :flow-id and :disable See `instrument-namespaces-clj` for :disable" ([var-symb] (instrument-var-clj var-symb {})) ([var-symb config] (utils/ensure-vanilla) (dbg-api/vanilla-instrument-var :clj var-symb config))) (defn uninstrument-var-clj "Remove instrumentation given a var symbol. (uninstrument-var-clj var-symb)" [var-symb] (utils/ensure-vanilla) (dbg-api/vanilla-uninstrument-var :clj var-symb {})) (defn instrument-namespaces-clj "Instrument all forms, in all namespaces that matches `prefixes`. `prefixes` is a set of ns prefixes like #{\"cljs.compiler\" \"cljs.analyzer\"} `opts` is a map containing : - :excluding-ns a set of strings with namespaces that should be excluded - :disable a set containing any of #{:expr :binding :anonymous-fn} useful for disabling unnecesary traces in code that generate too many traces - :verbose? when true show more logging " ([prefixes] (instrument-namespaces-clj prefixes {})) ([prefixes opts] (utils/ensure-vanilla) (dbg-api/vanilla-instrument-namespaces :clj prefixes opts))) (defn uninstrument-namespaces-clj "Undo instrumentation made by `flow-storm.api/instrument-namespaces-clj`" ([prefixes] (uninstrument-namespaces-clj prefixes {})) ([prefixes opts] (utils/ensure-vanilla) (dbg-api/vanilla-uninstrument-namespaces :clj prefixes opts))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ClojureScript instrumentation ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn instrument-var-cljs "Like `flow-storm.api/vanilla-instrument-var-clj` but for using it from the shadow Clojure repl. Arguments are the same as the Clojure version but `config` also accepts a `:build-id`" ([var-symb] (instrument-var-cljs var-symb {})) ([var-symb config] (dbg-api/vanilla-instrument-var :cljs var-symb config))) (defn uninstrument-var-cljs "Like `flow-storm.api/uninstrument-var-clj` but for using it from the shadow Clojure repl. Arguments are the same as the Clojure version but `config` needs a `:build-id`" [var-symb config] (dbg-api/vanilla-uninstrument-var :cljs var-symb config)) (defn instrument-namespaces-cljs "Like `flow-storm.api/instrument-namespaces-clj` but for using it from the shadow Clojure repl. Arguments are the same as the Clojure version but `config` also accepts a `:build-id`" ([prefixes] (instrument-namespaces-clj prefixes {})) ([prefixes opts] (dbg-api/vanilla-instrument-namespaces :cljs prefixes opts))) (defn uninstrument-namespaces-cljs "Like `flow-storm.api/uninstrument-namespaces-clj` but for using it from the shadow Clojure repl. Arguments are the same as the Clojure version but `config` also accepts a `:build-id`" [prefixes config] (dbg-api/vanilla-uninstrument-namespaces :cljs prefixes config)) (defn- runi* [{:keys [ns flow-id env] :as opts} form] ;; ~'flowstorm-runi is so it doesn't expand into flow-storm.api/flowstorm-runi which ;; doesn't work with fn* in clojurescript (utils/ensure-vanilla) (let [wrapped-form `(fn* ~'flowstorm-runi ([] ~form)) ns (or ns (when-let [env-ns (-> env :ns :name)] (str env-ns))) hansel-config (assoc (tracer/hansel-config opts) :env env) {:keys [inst-form init-forms]} (hansel/instrument-form hansel-config wrapped-form)] `(let [flow-id# ~(or flow-id (-> form meta :flow-id) 0) curr-ns# ~(or ns `(when *ns* (str (ns-name *ns*))) (-> env :ns :name str))] ~@init-forms (~inst-form)))) (defmacro runi "Run instrumented. (runi opts form) Instrument form and run it for tracing. Same as doing #rtrace `form`. `opts` is a map that support the same keys as `instrument-var`. " [opts form] (runi* (assoc opts :env &env) form)) (defmacro instrument* [config form] (let [env &env compiler (inst-utils/compiler-from-env env) ;; full-instr-form contains (do (trace-init-form ...) instr-form) {:keys [inst-form init-forms]} (hansel/instrument-form (merge config (tracer/hansel-config config) {:env env :form-file *file* :form-line (:line (meta form))}) form)] (if (and (= compiler :clj) (inst-utils/expanded-defn-form? inst-form)) ;; if we are in clojure and it is a (defn ...) or (def . (fn [])) ;; add a watch to its var to track when it is being instrumented/uninstrumented (let [var-symb (second form)] `(do ~@init-forms ~inst-form (let [v# (var ~var-symb) [vns# vname#] ((juxt namespace name) (symbol v#))] (rt-events/publish-event! (rt-events/make-vanilla-var-instrumented-event vname# vns#)) (add-watch v# :flow-storm/var-redef (fn [a1# a2# fn-before# fn-after#] (cond (and (:hansel/instrumented? (meta fn-before#)) (not (:hansel/instrumented? (meta fn-after#)))) (rt-events/publish-event! (rt-events/make-vanilla-var-uninstrumented-event vname# vns#)) (and (not (:hansel/instrumented? (meta fn-before#))) (:hansel/instrumented? (meta fn-after#))) (rt-events/publish-event! (rt-events/make-vanilla-var-instrumented-event vname# vns#)))))))) `(do ~@init-forms ~inst-form)))) (defn show-doc [var-symb] (rt-events/publish-event! (rt-events/show-doc-event var-symb))) (defn read-trace-tag [form] `(instrument* {} ~form)) (defn read-ctrace-tag [form] `(instrument* {:tracing-disabled? true} ~form)) (defn- read-rtrace-tag* [config form] (let [full-config (merge config (meta form))] `(cond (utils/storm-env?) (throw (ex-info "#rtrace and #trace can't be used with ClojureStorm, they aren't needed. All your configured compilations will be automatically instrumented. Please re-run the expression without it. Evaluation skipped." {})) (not (tracer/recording?)) (do (log "FlowStorm recording is paused, please switch recording on before running with #rtrace.") ~form) :else (let [_# (dbg-api/discard-flow (tracer/get-current-flow-id)) res# (runi ~full-config ~form)] (dbg-api/jump-to-last-expression-in-this-thread) res#)))) (defn read-rtrace-tag [form] (read-rtrace-tag* {} form)) (defn read-tap-tag [form] `(let [form-val# ~form] (tap> form-val#) form-val# )) (defn current-stack-trace [] (try (throw (Exception. "Dummy")) (catch Exception e (->> (with-out-str (stacktrace/print-stack-trace e)) (str/split-lines) seq (drop 3))))) (defn read-tap-stack-trace-tag [form] `(do (tap> (flow-storm.api/current-stack-trace)) ~form)) (defn break-at ([fq-fn-symb] (dbg-api/add-breakpoint! fq-fn-symb {})) ([fq-fn-symb args-pred] (dbg-api/add-breakpoint! fq-fn-symb {} args-pred))) (defn remove-break [fq-fn-symb] (dbg-api/remove-breakpoint! fq-fn-symb {})) (def unblock-thread dbg-api/unblock-thread) (def unblock-all-threads dbg-api/unblock-all-threads) (def clear-breaks dbg-api/clear-breakpoints!) (defn start-recording [] (dbg-api/set-recording true)) (defn stop-recording [] (dbg-api/set-recording false)) (defn set-before-reload-callback! [cb] (reload-utils/set-before-reload-callback! cb)) (defn set-after-reload-callback! [cb] (reload-utils/set-after-reload-callback! cb)) (defn data-window-push-val ([dw-id val] (data-window-push-val dw-id val nil)) ([dw-id val stack-key] (data-window-push-val dw-id val stack-key nil)) ([dw-id val stack-key extra] (let [vdata (rt-values/extract-data-aspects val extra) extra (assoc extra :dw-id dw-id :stack-key stack-key :root? true)] (rt-events/publish-event! (rt-events/make-data-window-push-val-data-event dw-id vdata extra))))) (defn data-window-val-update [dw-id new-val] (rt-events/publish-event! (rt-events/make-data-window-update-event dw-id {:new-val new-val}))) (defmacro bookmark ([note] `(vary-meta (symbol "flow-storm" "bookmark") assoc :flow-storm.bookmark/note ~(str note))) ([] `(bookmark nil))) (intern 'clojure.core (with-meta 'bookmark {:macro true}) @#'bookmark) (intern 'clojure.core (with-meta 'debugger {:macro true}) @#'bookmark) (defn probe-ref "Sample a *ref and send it to an oscilloscope data window. ch1-f and ch2-f are functions of one argument, a snapshot of the ref, that will determine the values of the two scope channels." [*ref ch1-f ch2-f {:keys [samp-rate dw-key] :or {samp-rate 200, dw-key :scope-probe}}] (data-window-push-val dw-key (reify rt-values/ScopeFrameP (frame-samp-rate [_] samp-rate) (frame-samples [_]))) (let [frame-size 32 sample-nanos (/ 1e9 samp-rate) samp-thread (doto (Thread. (fn [] (try (while (not (Thread/interrupted)) (loop [last-sample-nanos (System/nanoTime) frame-samples (transient [])] (if (= frame-size (count frame-samples)) (data-window-val-update dw-key (reify rt-values/ScopeFrameP (frame-samp-rate [_] samp-rate) (frame-samples [_] (persistent! frame-samples)))) (let [ref-val (deref *ref) samp (reify rt-values/ScopeFrameSampleP (sample-chan-1 [_] (ch1-f ref-val)) (sample-chan-2 [_] (ch2-f ref-val))) now (System/nanoTime) delta (- now last-sample-nanos) sync-to-samp-rate-nanos (- sample-nanos delta) sleep-millis (long (quot sync-to-samp-rate-nanos 1e6))] (when (pos? sleep-millis) (Thread/sleep sleep-millis)) (recur now (conj! frame-samples samp)))))) (catch Exception e (.printStackTrace e))))) (.start))] (fn [] (.interrupt samp-thread)))) ================================================ FILE: src-inst/flow_storm/api.cljs ================================================ (ns flow-storm.api (:require [flow-storm.remote-websocket-client :as remote-websocket-client] [flow-storm.runtime.outputs :as rt-outputs] [flow-storm.runtime.debuggers-api :as dbg-api] [flow-storm.runtime.events :as rt-events] [flow-storm.runtime.indexes.api :as indexes-api] [flow-storm.runtime.values :as rt-values] [flow-storm.utils :refer [log]] [hansel.instrument.runtime]) (:require-macros [flow-storm.api])) (defn stop "Stop the flow-storm runtime part gracefully" [] (rt-outputs/remove-tap!) (rt-events/clear-dispatch-fn!) (rt-events/clear-pending-events!) (rt-values/clear-vals-ref-registry) (indexes-api/stop) (remote-websocket-client/stop-remote-websocket-client) (log "System stopped")) (defn current-stack-trace "Utility that returns the current stack-trace" [] (rest (.split (.-stack (js/Error.)) "\n"))) (defn set-thread-trace-limit "Set a trace limit to all threads. When the limit is positive, if any thread timeline goes beyond the limit the thread code will throw an exception." [limit] (dbg-api/set-thread-trace-limit limit)) (defn data-window-push-val ([dw-id val] (data-window-push-val dw-id val nil)) ([dw-id val stack-key] (data-window-push-val dw-id val stack-key nil)) ([dw-id val stack-key extra] (let [vdata (rt-values/extract-data-aspects val extra) extra (assoc extra :dw-id dw-id :stack-key stack-key :root? true)] (rt-events/publish-event! (rt-events/make-data-window-push-val-data-event dw-id vdata extra))))) (defn data-window-val-update [dw-id new-val] (rt-events/publish-event! (rt-events/make-data-window-update-event dw-id {:new-val new-val}))) ================================================ FILE: src-inst/flow_storm/jobs.cljc ================================================ (ns flow-storm.jobs (:require [flow-storm.runtime.events :as rt-events] [flow-storm.utils :as utils :refer [log]] [flow-storm.runtime.indexes.api :as indexes-api] [clojure.set :as set]) #?(:clj (:import [java.util.concurrent Executors TimeUnit]))) (def mem-reporter-interval 1000) (def timeline-updates-check-interval 2000) (defonce cancel-jobs-fn (atom nil)) #?(:clj (defonce scheduled-thread-pool (Executors/newScheduledThreadPool 1))) #?(:clj (defn schedule-repeating-fn [f millis] (let [sched-feature (.scheduleAtFixedRate scheduled-thread-pool f millis millis TimeUnit/MILLISECONDS)] (log (str f " function scheduled every " millis " millis")) (fn [] (.cancel sched-feature false) (log (str f " scheduled function cancelled."))))) :cljs (defn schedule-repeating-fn [f millis] (let [interval-id (js/setInterval f millis)] (log (str (.-name f) " function scheduled every " millis " millis")) (fn [] (js/clearInterval interval-id) (log (str (.-name f) " scheduled function cancelled.")))))) (defn run-jobs [] (let [mem-job-cancel (schedule-repeating-fn (fn mem-reporter [] (let [heap-info (utils/get-memory-info) ev (rt-events/make-heap-info-update-event heap-info)] (rt-events/publish-event! ev))) mem-reporter-interval) last-checked-stamps (atom nil) updates-job-cancel (schedule-repeating-fn (fn timelines-updates [] (let [new-stamps (indexes-api/timelines-mod-timestamps) needs-report (set/difference new-stamps @last-checked-stamps)] (doseq [{:keys [flow-id thread-id] } needs-report] (rt-events/publish-event! (rt-events/make-timeline-updated-event flow-id thread-id))) (reset! last-checked-stamps new-stamps))) timeline-updates-check-interval)] (reset! cancel-jobs-fn (fn [] (mem-job-cancel) (updates-job-cancel))))) (defn stop-jobs [] (when-let [stop-fn @cancel-jobs-fn] (stop-fn))) (comment (indexes-api/timelines-mod-timestamps) ) ================================================ FILE: src-inst/flow_storm/nrepl/middleware.clj ================================================ (ns flow-storm.nrepl.middleware (:require [flow-storm.runtime.debuggers-api :as debuggers-api] [flow-storm.types :refer [make-value-ref value-ref?]] [flow-storm.runtime.outputs :as rt-outputs] [nrepl.misc :refer [response-for] :as misc] [nrepl.middleware :as middleware :refer [set-descriptor!]] [nrepl.transport :as t] [clojure.java.io :as io] [clojure.string :as str] [flow-storm.form-pprinter :as form-pprinter] [nrepl.middleware.caught :as caught :refer [wrap-caught]] [nrepl.middleware.print :refer [wrap-print]] [nrepl.middleware.interruptible-eval :refer [*msg*]]) (:import [nrepl.transport Transport])) (defn value-ref->int [m k] (if-let [vr (get m k)] (cond (value-ref? vr) (update m k :vid) ;; this is because whatever reads values from the js runtime ;; (the piggieback path) doesn't use the data readers in data_readers.clj ;; so it will read flow-storm.types/value as default tagged-litterals (and (tagged-literal? vr) (= 'flow-storm.types/value-ref (:tag vr))) (update m k :form) :else m) m)) (defn trace-count [{:keys [flow-id thread-id]}] {:code `(debuggers-api/timeline-count ~(or flow-id 0) ~thread-id) :post-proc (fn [cnt] {:status :done :trace-cnt cnt})}) (defn find-fn-call [{:keys [flow-id fq-fn-symb from-idx backward]}] {:code `(debuggers-api/find-fn-call-sync ~flow-id (symbol ~fq-fn-symb) ~from-idx ~(Boolean/parseBoolean backward)) :post-proc (fn [fn-call] {:status :done :fn-call (value-ref->int fn-call :fn-args)})}) (defn find-flow-fn-call [{:keys [flow-id]}] {:code `(debuggers-api/find-flow-fn-call ~(if (number? flow-id) flow-id nil)) :post-proc (fn [fn-call] {:status :done :fn-call (value-ref->int fn-call :fn-args)})}) (defn get-form [{:keys [form-id]}] {:code `(debuggers-api/get-form ~form-id) :post-proc (fn [form] (let [{:keys [form/id form/form form/ns form/def-kind form/file form/line]} form file-path (when-let [f (when (and file (not= file "NO_SOURCE_PATH")) (if (or (str/starts-with? file "/") ;; it is a unix absolute path (re-find #"^[a-zA-Z]:[\\/].+" file)) ;; it is a windows absolute path (io/file file) ;; if form/file is not an absolute path then it is a resource (io/resource file)))] (.getPath f))] {:status :done :form {:id id :ns ns :def-kind def-kind :line line :pprint (form-pprinter/code-pprint form) :file file-path}}))}) (defn timeline-entry [{:keys [flow-id thread-id idx drift]}] {:code `(debuggers-api/timeline-entry ~(if (number? flow-id) flow-id nil) ~thread-id ~idx ~(keyword drift)) :post-proc (fn [entry] {:status :done :entry (-> entry (value-ref->int :fn-args) (value-ref->int :result))})}) (defn frame-data [{:keys [flow-id thread-id fn-call-idx]}] {:code `(debuggers-api/frame-data ~(if (number? flow-id) flow-id nil) ~thread-id ~fn-call-idx {}) :post-proc (fn [frame] {:status :done :frame (-> frame (value-ref->int :args-vec) (value-ref->int :ret))})}) (defn pprint-val-ref [{:keys [val-ref print-level print-length print-meta pprint]}] {:code `(debuggers-api/val-pprint (make-value-ref ~val-ref) {:print-length ~print-length :print-level ~print-level :print-meta? ~(Boolean/parseBoolean print-meta) :pprint? ~(Boolean/parseBoolean pprint)}) :post-proc (fn [pprint-str] {:status :done :pprint pprint-str})}) (defn bindings [{:keys [flow-id thread-id idx]}] {:code `(debuggers-api/bindings ~(if (number? flow-id) flow-id nil) ~thread-id ~idx nil) :post-proc (fn [bindings] {:status :done :bindings (reduce-kv (fn [r k vref] (assoc r k (:vid vref))) {} bindings)})}) (defn toggle-recording [_] {:code `(debuggers-api/toggle-recording) :post-proc (fn [_] {:status :done})}) (defn clear-recordings [_] {:code `(debuggers-api/clear-flows) :post-proc (fn [_] {:status :done})}) (defn recorded-functions [_] {:code `(debuggers-api/all-fn-call-stats) :post-proc (fn [stats] {:status :done :functions (mapv (fn [[fq-fn-name cnt]] {:fq-fn-name fq-fn-name :cnt cnt}) stats)})}) (defn cljs-transport [{:keys [^Transport transport] :as msg} post-proc] (reify Transport (recv [_this] (.recv transport)) (recv [_this timeout] (.recv transport timeout)) (send [this response] (cond (contains? response :value) (let [rsp-val (:value response) ;; this is HACKY, but the ClojureScript middleware can ;; return (:value response) as a Map/Vector/etc or the thing as a String ;; if it contains things like [#flow-storm.types/value-ref 5] rsp-val (if (string? rsp-val) (read-string rsp-val) rsp-val) processed-val (post-proc rsp-val) rsp (response-for msg processed-val)] (.send transport rsp)) :else (.send transport response)) this))) (defn process-msg [next-handler {:keys [^Transport transport] :as msg} msg-proc-fn cljs?] (let [{:keys [code post-proc]} (msg-proc-fn msg)] (if cljs? ;; ClojureScript handling (let [tr (cljs-transport msg post-proc) cljs-code (pr-str code)] (next-handler (assoc msg :transport tr :op "eval" :code cljs-code :ns "cljs.user"))) ;; Clojure handling (let [rsp (response-for msg (post-proc (eval code)))] (t/send transport rsp))))) (defn- wrap-transport [transport op msg-id {:keys [on-eval on-out on-err]}] (reify Transport (recv [_this] (t/recv transport)) (recv [_this timeout] (t/recv transport timeout)) (send [this resp] (cond (:out resp) (on-out (:out resp)) (:err resp) (on-err (:err resp)) (and (= op "eval") (= msg-id (:id resp)) (contains? resp :value)) (on-eval (:value resp))) (.send transport resp) this))) (def cider-piggieback? (try (require 'cider.piggieback) true (catch Throwable _ false))) (def nrepl-piggieback? (try (require 'piggieback.core) true (catch Throwable _ false))) (defn try-piggieback "If piggieback is loaded, returns `#'cider.piggieback/wrap-cljs-repl`, or false otherwise." [] (cond cider-piggieback? (resolve 'cider.piggieback/wrap-cljs-repl) nrepl-piggieback? (resolve 'piggieback.core/wrap-cljs-repl) :else false)) (defn- maybe-piggieback [descriptor descriptor-key] (if-let [piggieback (try-piggieback)] (update-in descriptor [descriptor-key] #(set (conj % piggieback))) descriptor)) (defn expects-piggieback "If piggieback is loaded, returns the descriptor with piggieback's `wrap-cljs-repl` handler assoc'd into its `:expects` set." [descriptor] (maybe-piggieback descriptor :expects)) (defn wrap-flow-storm "Middleware that provides flow-storm functionality " [next-handler] (fn [{:keys [op id] :as msg}] (let [piggieback? (or cider-piggieback? nrepl-piggieback?)] (case op "flow-storm-trace-count" (process-msg next-handler msg trace-count piggieback?) "flow-storm-find-fn-call" (process-msg next-handler msg find-fn-call piggieback?) "flow-storm-find-flow-fn-call" (process-msg next-handler msg find-flow-fn-call piggieback?) "flow-storm-get-form" (process-msg next-handler msg get-form piggieback?) "flow-storm-timeline-entry" (process-msg next-handler msg timeline-entry piggieback?) "flow-storm-frame-data" (process-msg next-handler msg frame-data piggieback?) "flow-storm-pprint" (process-msg next-handler msg pprint-val-ref piggieback?) "flow-storm-toggle-recording" (process-msg next-handler msg toggle-recording piggieback?) "flow-storm-clear-recordings" (process-msg next-handler msg clear-recordings piggieback?) "flow-storm-bindings" (process-msg next-handler msg bindings piggieback?) "flow-storm-recorded-functions" (process-msg next-handler msg recorded-functions piggieback?) ;; if the message is not for us let it flow but with a transport ;; we control, so we can handle writes to out, err and eval results (let [msg (if piggieback? ;; don't do anything for ClojureScript yet msg ;; update transport to capture *out*, *err* and eval resluts (update msg :transport (fn [transport] (wrap-transport transport op id {:on-eval rt-outputs/handle-eval-result :on-out rt-outputs/handle-out-write :on-err rt-outputs/handle-err-write}))))] ;; TODO: remove binding *msg* ;; this isn't needed anymore for nrepl >= 1.3.1 (https://github.com/nrepl/nrepl/issues/363) ;; Let's leave it for some time since doesn't seams to hurt and allows people to use *out* and *err* ;; with 1.3.0 (binding [*msg* msg] (next-handler msg))))))) (defn expects-shadow-cljs-middleware "If shadow-cljs middleware is on the classpath, make sure we set our middleware before it." [descriptor] (if-let [shadow-middleware-var (try (requiring-resolve 'shadow.cljs.devtools.server.nrepl/middleware) (catch Throwable _ false))] (update descriptor :expects #(set (conj % shadow-middleware-var))) descriptor)) (def descriptor (expects-piggieback (expects-shadow-cljs-middleware {:requires #{"clone" #'wrap-caught #'wrap-print} :expects #{"eval"} :handles {"flow-storm-trace-count" {:doc "Get the traces count for a thread" :requires {"flow-id" "The id of the flow" "thread-id" "The id of the thread"} :optional {} :returns {"trace-cnt" "A number with the size of the recorded timeline"}} "flow-storm-find-fn-call" {:doc "Find the first FnCall for a symbol" :requires {"flow-id" "The id of the flow" "fq-fn-symb" "The Fully qualified function symbol" "from-idx" "The starting timeline idx to search from" "backward" "When true, searches for a fn-call by walking the timeline backwards"} :optional {} :returns {"fn-call" "A map like {:keys [fn-name fn-ns form-id fn-args fn-call-idx idx parent-idx ret-idx flow-id thread-id]}"}} "flow-storm-find-flow-fn-call" {:doc "Find the first FnCall for a flow" :requires {"flow-id" "The id of the flow"} :optional {} :returns {"fn-call" "A map like {:keys [fn-name fn-ns form-id fn-args fn-call-idx idx parent-idx ret-idx]}"}} "flow-storm-get-form" {:doc "Return a registered form" :requires {"form-id" "The id of the form"} :optional {} :returns {"form" "A map with {:keys [id ns def-kind line pprint file]}"}} "flow-storm-timeline-entry" {:doc "Return a timeline entry" :requires {"flow-id" "The flow-id for the entry" "thread-id" "The thread-id for the entry" "idx" "The current timeline idx" "drift" "The drift, one of next-out next-over prev-over next prev at"} :optional {} :returns {"entry" (str "One of : " "FnCall {:keys [type fn-name fn-ns form-id fn-args fn-call-idx idx parent-idx ret-idx]}" "Expr {:keys [type coord result fn-call-idx idx]}" "FnReturn {:keys [type coord result fn-call-idx idx]}")}} "flow-storm-frame-data" {:doc "Return a frame for a fn-call index" :requires {"flow-id" "The flow-id for the entry" "thread-id" "The thread-id for the entry" "fn-call-idx" "The fn-call timeline idx"} :optional {} :returns {"frame" "A map with {:keys [fn-ns fn-name args-vec ret form-id fn-call-idx parent-fn-call-idx]}"}} "flow-storm-pprint" {:doc "Return a pretty printing for a value reference id" :requires {"val-ref" "The value reference id" "print-length" "A *print-length* val for pprint" "print-level" "A *print-level* val for pprint" "print-meta" "A *print-meta* val for pprint" "pprint" "When true will pretty print, otherwise just print"} :optional {} :returns {"pprint" "A map with {:keys [val-str val-type]}"}} "flow-storm-bindings" {:doc "Return all bindings for the provided idx" :requires {"flow-id" "The id of the flow" "thread-id" "The id of the thread" "idx" "The current timeline index"} :optional {} :returns {"bindings" "A map with {:keys [bind-symb val-ref-id]}"}} "flow-storm-toggle-recording" {:doc "Toggles recording on/off" :requires {} :optional {} :returns {}} "flow-storm-clear-recordings" {:doc "Clears all flows recordings" :requires {} :optional {} :returns {}} "flow-storm-recorded-functions" {:doc "Return all the functions there are recordings for" :requires {} :optional {} :returns {"functions" "A collection of maps like {:keys [fq-fn-name cnt]}"}}}}))) (set-descriptor! #'wrap-flow-storm descriptor) (comment ;; For testing middlewares #_:clj-kondo/ignore (let [flow-id 0 thread-id 30 p (promise) h (wrap-flow-storm (constantly true))] (with-redefs [t/send (fn [_ rsp] (deliver p rsp))] #_(h {:op "flow-storm-trace-count" :flow-id flow-id :thread-id thread-id}) (h {:flow-id flow-id :op "flow-storm-find-fn-call" :fq-fn-symb "dev-tester/factorial"}) #_(h {:op "flow-storm-find-flow-fn-call" :flow-id flow-id}) #_(h {:op "flow-storm-get-form" :form-id -798068730}) #_(h {:op "flow-storm-timeline-entry" :flow-id flow-id :thread-id thread-id :idx 3 :drift "at"}) #_(h {:op "flow-storm-frame-data" :flow-id flow-id :thread-id thread-id :fn-call-idx 0}) #_(h {:op "flow-storm-pprint" :val-ref 5}) #_(h {:op "flow-storm-bindings" :flow-id flow-id :thread-id thread-id :idx 8}) #_(h {:op "flow-storm-toggle-recording"}) #_(h {:op "flow-storm-recorded-functions"}) #_(h {:op "flow-storm-clear-recordings"}) (deref p 1000 :no-response))) ) ================================================ FILE: src-inst/flow_storm/ns_reload_utils.clj ================================================ ;; Most functions here were copied from the amazing clj-reload by Nikita Prokopov (Tonsky) ;; https://github.com/tonsky/clj-reload ;; MIT License ;; Copyright (c) 2024 Nikita Prokopov ;; Permission is hereby granted, free of charge, to any person obtaining a copy ;; of this software and associated documentation files (the "Software"), to deal ;; in the Software without restriction, including without limitation the rights ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ;; copies of the Software, and to permit persons to whom the Software is ;; furnished to do so, subject to the following conditions: ;; The above copyright notice and this permission notice shall be included in all ;; copies or substantial portions of the Software. ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ;; SOFTWARE. (ns flow-storm.ns-reload-utils (:require [clojure.string :as str] [clojure.java.io :as io] [clojure.walk :as walk] [flow-storm.utils :refer [log]]) (:import [clojure.lang LineNumberingPushbackReader] [java.io File StringReader] [java.net URL])) (defn- file-reader ^LineNumberingPushbackReader [f] (LineNumberingPushbackReader. (io/reader (io/file f)))) (defn- string-reader ^LineNumberingPushbackReader [^String s] (LineNumberingPushbackReader. (StringReader. s))) (defn- throwable? [o] (instance? Throwable o)) (defn- update! [m k f & args] (assoc! m k (apply f (m k) args))) (def conjs (fnil conj #{})) (def intos (fnil into #{})) (defn- assoc-some [m & kvs] (reduce (fn [m [k v]] (cond-> m (some? v) (assoc k v))) m (partition 2 kvs))) (defn- some-set [& vals] (not-empty (set (filter some? vals)))) (def dummy-resolver (reify clojure.lang.LispReader$Resolver (currentNS [_] 'user) (resolveClass [_ sym] sym) (resolveAlias [_ sym] sym) (resolveVar [_ sym] sym))) (def reader-opts {:read-cond :allow :features #{:clj} :eof ::eof}) (defn- read-form [reader] (binding [*suppress-read* true *reader-resolver* dummy-resolver] (read reader-opts reader))) (defn- parse-require-form [form] (loop [body (next form) result (transient #{})] (let [[decl & body'] body] (cond (empty? body) (persistent! result) (symbol? decl) ;; a.b.c (recur body' (conj! result decl)) (not (sequential? decl)) (do (log "Unexpected" (first form) "form:" (pr-str decl)) (recur body' result)) (not (symbol? (first decl))) (do (log "Unexpected" (first form) "form:" (pr-str decl)) (recur body' result)) (or (nil? (second decl)) ;; [a.b.d] (keyword? (second decl))) ;; [a.b.e :as e] (if (= :as-alias (second decl)) ;; [a.b.e :as-alias e] (recur body' result) (recur body' (conj! result (first decl)))) :else ;; [a.b f [g :as g]] (let [prefix (first decl) symbols (->> (next decl) (remove #(and (sequential? %) (= :as-alias (second %)))) ;; [a.b [g :as-alias g]] (map #(if (symbol? %) % (first %))) (map #(symbol (str (name prefix) "." (name %)))))] (recur body' (reduce conj! result symbols))))))) (defn- parse-ns-form [form] (let [name (second form)] (loop [body (nnext form) requires (transient #{})] (let [[form & body'] body tag (when (list? form) (first form))] (cond (empty? body) [name (not-empty (persistent! requires))] (#{:require :use} tag) (recur body' (reduce conj! requires (parse-require-form form))) :else (recur body' requires)))))) (defn- expand-quotes [form] (walk/postwalk #(if (and (sequential? %) (not (vector? %)) (= 'quote (first %))) (second %) %) form)) (defn- read-file "Returns { NS} or Exception" ([file] (with-open [rdr (file-reader file)] (read-file rdr file))) ([rdr file] (try (loop [ns nil nses {}] (let [form (read-form rdr) tag (when (list? form) (first form))] (cond (= ::eof form) nses (= 'ns tag) (let [[ns requires] (parse-ns-form form) requires (disj requires ns)] (recur ns (update nses ns assoc-some :meta (meta ns) :requires requires :ns-files (some-set file)))) (= 'in-ns tag) (let [[_ ns] (expand-quotes form)] (recur ns (update nses ns assoc-some :in-ns-files (some-set file)))) (and (nil? ns) (#{'require 'use} tag)) (throw (ex-info (str "Unexpected " tag " before ns definition in " file) {:form form})) (#{'require 'use} tag) (let [requires' (parse-require-form (expand-quotes form)) requires' (disj requires' ns)] (recur ns (update-in nses [ns :requires] intos requires'))) (or (= 'defonce tag) (list? form)) (let [[_ name] form] (recur ns (assoc-in nses [ns :keep name] {:tag tag :form form}))) :else (recur ns nses)))) (catch Exception e (log "Failed to read" (.getPath file) (.getMessage e)) (ex-info (str "Failed to read" (.getPath file)) {:file file} e))))) (defn- read-file-or-resource [path] (try (when-let [f-url (or (io/resource path) (some-> (io/file path) (.toURL)))] (let [rdr (string-reader (slurp (io/reader f-url)))] (read-file rdr f-url))) (catch Exception _ (log "Failed to read" path "ignoring...") nil))) (defn- dependees "Inverts the requies graph. Returns {ns -> #{downstream-ns ...}}" [namespaces] (let [*m (volatile! (transient {}))] (doseq [[from {tos :requires}] namespaces] (vswap! *m update! from #(or % #{})) (doseq [to tos :when (namespaces to)] (vswap! *m update! to conjs from))) (persistent! @*m))) (declare topo-sort) (defn- report-cycle [deps all-deps] (let [circular (filterv (fn [node] (try (topo-sort (dissoc deps node) (fn [_ _] (throw (ex-info "Part of cycle" {})))) true (catch Exception _ false))) (keys deps))] (throw (ex-info (str "Cycle detected: " (str/join ", " (sort circular))) {:nodes circular :deps all-deps})))) (defn- topo-sort ([deps] (topo-sort deps report-cycle)) ([all-deps on-cycle] (loop [res (transient []) deps all-deps] (if (empty? deps) (persistent! res) (let [ends (reduce into #{} (vals deps)) roots (->> (keys deps) (remove ends) (sort))] (if (seq roots) (recur (reduce conj! res roots) (reduce dissoc deps roots)) (on-cycle deps all-deps))))))) (defn- topo-sort-fn "Accepts dependees map {ns -> #{downsteram-ns ...}}, returns a fn that topologically sorts dependencies" [deps] (let [sorted (topo-sort deps)] (fn [coll] (filter (set coll) sorted)))) (defn- deep-dependees-set "Given a set of some initial namespaces and a dependees map like the one calculated by `dependees`, return a set off all trasitively reached namespaces including those on the initial set (initial-nss)." [initial-nss ns-dependees] (->> initial-nss (mapcat (fn [ns] (deep-dependees-set (get ns-dependees ns) ns-dependees) )) (reduce conj initial-nss))) (defn- ns-unload [ns] (log "Unloading" ns) (remove-ns ns) (dosync #_:clj-kondo/ignore (alter @#'clojure.core/*loaded-libs* disj ns))) (defn- ns-load-file [content ns file-name] (let [[_ ext] (re-matches #".*\.([^.]+)" file-name) path (-> ns str (str/replace #"\-" "_") (str/replace #"\." "/") (str "." ext))] (Compiler/load (StringReader. content) path file-name))) (defn- ns-load [ns file-or-url] (log "Loading" ns) (try (ns-load-file (slurp file-or-url) ns (if (instance? java.io.File file-or-url) (.getName ^File file-or-url) (.getFile ^URL file-or-url))) nil (catch Throwable t (log " failed to load" ns t) t))) (def before-reload-callback nil) (def after-reload-callback nil) (defn set-before-reload-callback! [cb] (alter-var-root #'before-reload-callback (constantly cb))) (defn set-after-reload-callback! [cb] (alter-var-root #'after-reload-callback (constantly cb))) (defn reload-all "Reload all loaded namespaces that contains at least one var, which matches regex, and any other namespaces depending on them." [regex] (let [;; collect all loaded namespaces resources files paths set all-paths (->> (all-ns) (reduce (fn [files ns] (reduce (fn [files' ns-var] (if-let [f-path (some-> ns-var meta :file)] (conj files' f-path) files')) files (vals (ns-interns ns)))) #{})) ;; build the namespaces map namespaces (reduce (fn [nss path] (let [res (read-file-or-resource path)] ;; throwables here are caused for example by reading ;; files which contains "#{`ns 'ns}". This is because ;; of reading with dummy-resolver (if-not (throwable? res) (merge nss res) nss))) {} all-paths) dependees (dependees namespaces) topo-sort (topo-sort-fn dependees) matched-ns (->> (keys namespaces) (filterv (fn [ns] (re-matches regex (name ns)))) (into #{})) to-reload (->> (deep-dependees-set matched-ns dependees) topo-sort) to-unload (reverse to-reload)] (when before-reload-callback (before-reload-callback)) (doseq [ns to-unload] (ns-unload ns)) (doseq [ns to-reload] (doseq [ns-files (get-in namespaces [ns :ns-files])] (ns-load ns ns-files))) (when after-reload-callback (after-reload-callback)))) (comment (reload-all #"hanse.*") ) ================================================ FILE: src-inst/flow_storm/preload.cljs ================================================ (ns flow-storm.preload (:require [flow-storm.runtime.debuggers-api :as dbg-api])) (def dbg-port (js/parseInt (if js/window (let [page-params (-> js/window .-location .-search) url-params (js/URLSearchParams. page-params)] (or (.get url-params "flowstorm_ws_port") "7722")) ;; for node js "7722"))) (dbg-api/start-runtime) (dbg-api/remote-connect {:debugger-host "localhost" :debugger-ws-port dbg-port}) ================================================ FILE: src-inst/flow_storm/remote_websocket_client.clj ================================================ (ns flow-storm.remote-websocket-client (:refer-clojure :exclude [send]) (:require [flow-storm.json-serializer :as serializer] [flow-storm.utils :refer [log log-error] :as utils]) (:import [org.java_websocket.client WebSocketClient] [java.net URI] [org.java_websocket.handshake ServerHandshake])) (def remote-websocket-client nil) (defn stop-remote-websocket-client [] (when remote-websocket-client (.close remote-websocket-client)) (alter-var-root #'remote-websocket-client (constantly nil))) (defn remote-connected? [] (boolean remote-websocket-client)) (defn send [ser-packet] ;; websocket library isn't clear about thread safty of send ;; lets synchronize just in case (locking remote-websocket-client (.send ^WebSocketClient remote-websocket-client ^String ser-packet))) (defn send-event-to-debugger [ev-packet] (let [ser-packet (serializer/serialize [:event ev-packet])] (send ser-packet))) (defn start-remote-websocket-client [{:keys [debugger-host debugger-ws-port on-connected api-call-fn]}] (let [debugger-host (or debugger-host "localhost") debugger-ws-port (or debugger-ws-port 7722) uri-str (format "ws://%s:%s/ws" debugger-host debugger-ws-port) _ (log (str "About to connect to " uri-str)) ^WebSocketClient ws-client (proxy [WebSocketClient] [(URI. uri-str)] (onOpen [^ServerHandshake handshake-data] (log (format "Connection opened to %s" uri-str)) (when on-connected (on-connected))) (onMessage [^String message] (let [[packet-key :as in-packet] (serializer/deserialize message) ret-packet (case packet-key :api-request (let [[_ request-id method args] in-packet] (try (let [ret-data (api-call-fn method args)] [:api-response [request-id nil ret-data]]) (catch Exception e [:api-response [request-id (.getMessage e) nil]]))) (log-error "Unrecognized packet key")) ret-packet-ser (serializer/serialize ret-packet)] (.send remote-websocket-client ret-packet-ser))) (onClose [code reason remote?] (log (format "Connection with %s closed. code=%s reson=%s remote?=%s" uri-str code reason remote?)) ((requiring-resolve 'flow-storm.runtime.debuggers-api/stop-runtime))) (onError [^Exception e] (log-error (format "WebSocket error connection %s" uri-str) e)))] (.setConnectionLostTimeout ws-client 0) (.connect ws-client) (alter-var-root #'remote-websocket-client (constantly ws-client)) ws-client)) ================================================ FILE: src-inst/flow_storm/remote_websocket_client.cljs ================================================ (ns flow-storm.remote-websocket-client (:require [flow-storm.utils :refer [log log-error] :as utils] [flow-storm.json-serializer :as serializer])) (def remote-websocket-client nil) (defn stop-remote-websocket-client [] (when remote-websocket-client (.close remote-websocket-client)) (set! remote-websocket-client nil)) (defn websocket-state [ws] (case (.-readyState ws) 0 :connecting 1 :open 2 :closing 3 :closed)) (defn remote-connected? [] (and remote-websocket-client (= :open (websocket-state remote-websocket-client)))) (defn web-socket-client-object [uri-str] (let [WebSocket (if (and (= *target* "nodejs") (exists? js/require)) (let [obj (try (js/require "websocket") (catch :default e (js/console.error "websocket node dependency not installed. Please npm install websocket to use flowstorm with nodejs" e)))] (.-w3cwebsocket ^js obj)) js/globalThis.WebSocket) ws-client (WebSocket. uri-str)] ws-client)) (defn send [ser-packet] (.send remote-websocket-client ser-packet)) (defn send-event-to-debugger [ev-packet] (let [ser-packet (serializer/serialize [:event ev-packet])] (send ser-packet))) (defn start-remote-websocket-client [{:keys [debugger-host debugger-ws-port on-connected api-call-fn]}] (if (remote-connected?) (js/console.warn "Websocket already connected. Skipping.") (let [debugger-host (or debugger-host "localhost") debugger-ws-port (or debugger-ws-port 7722) uri-str (utils/format "ws://%s:%s/ws" debugger-host debugger-ws-port) _ (log (str "About to connect to " uri-str)) ws-client (try (web-socket-client-object uri-str) (catch js/Error e (js/console.error (str "Can't connect to " uri-str) e)))] (set! (.-onerror ws-client) (fn [] (log-error (utils/format "WebSocket error connection %s" uri-str)))) (set! (.-onopen ws-client) (fn [] (log (utils/format "Connection opened to %s" uri-str)) (when on-connected (on-connected)))) (set! (.-onclose ws-client) (fn [] (log (utils/format "Connection with %s closed." uri-str)))) (set! (.-onmessage ws-client) (fn [msg] (try (if (= (.-type msg) "message") (let [message (.-data msg) [packet-key :as in-packet] (serializer/deserialize message) ret-packet (case packet-key :api-request (let [[_ request-id method args] in-packet] (try (let [ret-data (api-call-fn method args)] [:api-response [request-id nil ret-data]]) (catch js/Error err (log-error (str "Error on api-call-fn " [method args]) err) [:api-response [request-id (.-message err) nil]]))) (log-error "Unrecognized packet key")) ret-packet-ser (serializer/serialize ret-packet)] (.send ws-client ret-packet-ser)) (js/console.error (str "Message type not handled" msg))) (catch js/Error err (log-error "Error processing message : " (.-message err)))))) (set! remote-websocket-client ws-client) ws-client))) ================================================ FILE: src-inst/flow_storm/runtime/debuggers_api.cljc ================================================ (ns flow-storm.runtime.debuggers-api (:require [flow-storm.runtime.indexes.api :as index-api] [flow-storm.runtime.indexes.timeline-index :refer [ensure-indexes]] [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.json-serializer :as serializer] #?@(:clj [[flow-storm.utils :as utils :refer [log]]] :cljs [[flow-storm.utils :as utils :refer [log] :refer-macros [env-prop]]]) [flow-storm.runtime.events :as rt-events] [flow-storm.runtime.values :as rt-values :refer [reference-value! deref-value]] [flow-storm.runtime.outputs :as rt-outputs] [flow-storm.remote-websocket-client :as remote-websocket-client] [flow-storm.runtime.indexes.total-order-timeline :as total-order-timeline] [flow-storm.jobs :as jobs] [flow-storm.tracer :as tracer] [clojure.string :as str] #?@(:clj [[hansel.api :as hansel] [hansel.instrument.utils :as hansel-inst-utils]]) [flow-storm.runtime.types.fn-return-trace :as fn-return-trace] [flow-storm.runtime.types.fn-call-trace :as fn-call-trace] [flow-storm.runtime.types.expr-trace :as expr-trace])) ;; Utilities for long interruptible tasks (def interruptible-tasks "A map from task-id -> interrupt-ch" (atom {})) (defn interrupt-all-tasks [] (doseq [[task-id {:keys [interrupt]}] @interruptible-tasks] (interrupt) (swap! interruptible-tasks dissoc task-id))) (defn start-task [task-id] (let [{:keys [start]} (get @interruptible-tasks task-id)] (start))) (defn submit-interruptible-task [{:keys [task-id start interrupt]}] ;; Let's only allow one interruptible task at a time for now (interrupt-all-tasks) (swap! interruptible-tasks assoc task-id {:start (fn [] #?(:clj (future (try (start) (catch Exception e (rt-events/publish-event! (rt-events/make-task-failed-event task-id (.getMessage e))) (utils/log-error "Task failed" e)))) :cljs (try (start) (catch js/Error e (rt-events/publish-event! (rt-events/make-task-failed-event task-id (.-message e))) (utils/log-error "Task failed" e))))) :interrupt (fn [] (interrupt) (utils/log (utils/format "Task %s interrupted" task-id)))}) (rt-events/publish-event! (rt-events/make-task-submitted-event task-id)) task-id) (defn submit-async-interruptible-batched-timelines-keep-task "Submits a timelines batched processing task. See `flow-storm.runtime.indexes.api/async-interruptible-batched-timelines-keep` for f docs. Doesn't start the task. Returns a task id. You can start it by calling `start-task` and interrupt it with `interrupt-task` providing the returned task id. Progress and end will be reported through the event system via task-progress-event and task-finished-event. task-progress-event will be called after each batch and carry {:keys [flow-id thread-id batch]} which are collected entries for the batch just processed. Interruption can only happen between batches. timelines are the subset of timelines to be processed and can be built using `timelines-for`" [timelines f] (let [task-id (str (utils/rnd-uuid)) task (index-api/async-interruptible-batched-timelines-keep f timelines {:on-batch (fn [batch] (rt-events/publish-event! (rt-events/make-task-progress-event task-id {:batch batch}))) :on-end (fn [] (rt-events/publish-event! (rt-events/make-task-finished-event task-id)) (swap! interruptible-tasks dissoc task-id))})] (submit-interruptible-task (assoc task :task-id task-id)))) (defn submit-find-interruptible-task "Submits a timelines find entry tasks. Returns a task id. Doesn't start the task. You can start it by calling `start-task` and interrupt it with `interrupt-task` providing the returned task id. Progress, match and end will be reported through the event system via task-progress-event and task-finished-event." [pred fid-tid-timelines config {:keys [result-transform]}] (let [task-id (str (utils/rnd-uuid)) result-transform (or result-transform identity) task (index-api/timelines-async-interruptible-find-entry pred fid-tid-timelines config {:on-progress (fn [perc] (rt-events/publish-event! (rt-events/make-task-progress-event task-id {:progress perc}))) :on-match (fn [match-val] (rt-events/publish-event! (rt-events/make-task-finished-event task-id (result-transform match-val))) (swap! interruptible-tasks dissoc task-id)) :on-end (fn [] (rt-events/publish-event! (rt-events/make-task-finished-event task-id)) (swap! interruptible-tasks dissoc task-id))})] (submit-interruptible-task (assoc task :task-id task-id)))) #?(:clj (defn get-storm-instrumentation [kind] {:instrument-only-prefixes (try (case kind :clj (->> (utils/call-jvm-method "clojure.storm.Emitter" "getInstrumentationOnlyPrefixes" []) (mapv (fn [p] (clojure.lang.Compiler/demunge p)))) :cljs (into [] ((requiring-resolve 'cljs.storm.api/get-instr-prefixes)))) (catch Exception _ [])) :skip-prefixes (try (case kind :clj (->> (utils/call-jvm-method "clojure.storm.Emitter" "getInstrumentationSkipPrefixes" []) (mapv (fn [p] (clojure.lang.Compiler/demunge p)))) :cljs (into [] ((requiring-resolve 'cljs.storm.api/get-skip-prefixes)))) (catch Exception _ [])) :skip-regex (try (case kind :clj (when-let [pat (utils/call-jvm-method "clojure.storm.Emitter" "getInstrumentationSkipRegex" [])] (.pattern pat)) :cljs (when-let [pat ((requiring-resolve 'cljs.storm.api/get-skip-regex))] (.pattern pat))) (catch Exception _ nil))})) #?(:clj (defn storm-instrumentation-enable? [env-kind] (case env-kind :clj (utils/call-jvm-method "clojure.storm.Emitter" "getInstrumentationEnable" []) :cljs (deref (requiring-resolve 'cljs.storm.emitter/instrument-enable))))) #?(:clj (defn turn-storm-instrumentation [env-kind on?] (case env-kind :clj (utils/call-jvm-method "clojure.storm.Emitter" "setInstrumentationEnable" [on?]) :cljs ((requiring-resolve 'cljs.storm.api/set-instrumentation) on?)))) (defn runtime-config [] (let [storm? (utils/storm-env?) env-kind #?(:clj :clj :cljs :cljs)] (cond-> {:env-kind env-kind :storm? storm? :flow-storm-nrepl-middleware? (utils/flow-storm-nrepl-middleware?) :recording? (tracer/recording?) :total-order-recording? (tracer/multi-timeline-recording?) :breakpoints (tracer/all-breakpoints)}))) (defn val-pprint [vref opts] (rt-values/val-pprint-ref vref opts)) (defn data-window-push-val-data [dw-id vref {:keys [update?] :as extras}] (let [v (rt-values/deref-value vref) vdata (rt-values/extract-data-aspects v extras)] (rt-events/publish-event! (if update? (rt-events/make-data-window-update-event dw-id vdata) (rt-events/make-data-window-push-val-data-event dw-id vdata extras))))) #?(:clj (def def-value rt-values/def-value)) (def tap-value rt-values/tap-value) (defn get-form [form-id] (let [form (index-api/get-form form-id)] (if (:multimethod/dispatch-val form) (update form :multimethod/dispatch-val reference-value!) form))) (defn timeline-count [flow-id thread-id] (let [timeline (index-api/get-timeline flow-id thread-id)] (count timeline))) (defn- reference-frame-data! [{:keys [dispatch-val return/kind] :as frame-data}] (cond-> frame-data true (dissoc :frame) dispatch-val (update :dispatch-val reference-value!) true (update :args-vec reference-value!) (= kind :return) (update :ret reference-value!) (= kind :unwind) (update :throwable reference-value!) true (update :bindings (fn [bs] (mapv #(update % :value reference-value!) bs))) true (update :expr-executions (fn [ee] (mapv #(update % :result reference-value!) ee))))) (defn reference-timeline-entry! [entry] (case (:type entry) :fn-call (update entry :fn-args reference-value!) :fn-return (update entry :result reference-value!) :fn-unwind (update entry :throwable reference-value!) :bind (update entry :value reference-value!) :expr (update entry :result reference-value!))) (defn timeline-entry [flow-id thread-id idx drift] (some-> (index-api/timeline-entry flow-id thread-id idx drift) reference-timeline-entry!)) (defn multi-thread-timeline-count [flow-id] (count (index-api/total-order-timeline flow-id))) (defn frame-data [flow-id thread-id idx opts] (let [frame-data (index-api/frame-data flow-id thread-id idx opts)] (reference-frame-data! frame-data))) (defn bindings [flow-id thread-id idx opts] (let [bs-map (index-api/bindings flow-id thread-id idx opts)] (reduce-kv (fn [bs s v] (assoc bs s (reference-value! v))) {} bs-map))) (defn callstack-tree-root-node [flow-id thread-id] (index-api/callstack-root-node flow-id thread-id)) (defn callstack-node-childs [[flow-id thread-id fn-call-idx]] (index-api/callstack-node-childs flow-id thread-id fn-call-idx)) (defn callstack-node-frame [[flow-id thread-id fn-call-idx]] (let [frame-data (index-api/callstack-node-frame-data flow-id thread-id fn-call-idx)] (reference-frame-data! frame-data))) (defn fn-call-stats [flow-id thread-id] (let [stats (->> (index-api/fn-call-stats flow-id thread-id) (mapv (fn [fstats] (update fstats :dispatch-val reference-value!))))] stats)) (defn collect-fn-frames-task [flow-id thread-id fn-ns fn-name form-id render-args render-ret?] (let [frame-keeper (index-api/make-frame-keeper flow-id thread-id fn-ns fn-name form-id) print-opts {:print-length 3 :print-level 3 :print-meta? false :pprint? false}] (submit-async-interruptible-batched-timelines-keep-task (index-api/timelines-for {:flow-id flow-id :thread-id thread-id}) (fn [_ idx tl-entry] (when-let [{:keys [args-vec ret throwable] :as fn-frame} (frame-keeper idx tl-entry)] (let [fr (-> fn-frame reference-frame-data! (assoc :args-vec-str (:val-str (rt-values/val-pprint args-vec (assoc print-opts :nth-elems render-args)))))] (if render-ret? (if throwable (assoc fr :throwable-str (ex-message throwable)) (assoc fr :ret-str (:val-str (rt-values/val-pprint ret print-opts)))) fr))))))) (defn find-fn-call-task [fq-fn-call-symb from-idx {:keys [backward? flow-id thread-id]}] (let [criteria (cond-> {:fn-ns (namespace fq-fn-call-symb) :fn-name (name fq-fn-call-symb) :from-idx from-idx :backward? backward?} flow-id (assoc :flow-id flow-id) thread-id (assoc :thread-id thread-id))] (submit-find-interruptible-task (index-api/build-find-fn-call-entry-predicate criteria) (index-api/timelines-for criteria) criteria {:result-transform reference-timeline-entry!}))) (defn find-flow-fn-call [flow-id] (some-> (index-api/find-flow-fn-call flow-id) reference-timeline-entry!)) (defn find-expr-entry-task [{:keys [identity-val equality-val fn-name] :as criteria}] (let [criteria (cond-> criteria identity-val (update :identity-val deref-value) equality-val (update :equality-val deref-value) true (assoc :needs-form-id? true)) search-pred (if fn-name (index-api/build-find-fn-call-entry-predicate criteria) (index-api/build-find-expr-entry-predicate criteria))] (submit-find-interruptible-task search-pred (index-api/timelines-for criteria) criteria {:result-transform reference-timeline-entry!}))) (defn total-order-timeline-task [{:keys [flow-id only-functions?]}] (let [timeline (index-api/total-order-timeline flow-id) detail-mapper (total-order-timeline/make-detailed-timeline-mapper index-api/forms-registry) keeper (if only-functions? (fn [thread-id tl-idx tl-entry] (when (fn-call-trace/fn-call-trace? tl-entry) (detail-mapper thread-id tl-idx tl-entry))) (fn [thread-id tl-idx tl-entry] (detail-mapper thread-id tl-idx tl-entry)))] (submit-async-interruptible-batched-timelines-keep-task [timeline] keeper))) (defn thread-prints-task [{:keys [flow-id thread-id printers]}] (let [flow-mt-timeline (index-api/total-order-timeline flow-id) use-mt-timeline? (and (nil? thread-id) (pos? (count flow-mt-timeline))) tp-keeper (index-api/make-thread-prints-keeper printers) timelines (if use-mt-timeline? [flow-mt-timeline] (index-api/timelines-for {:flow-id flow-id :thread-id thread-id}))] (submit-async-interruptible-batched-timelines-keep-task timelines tp-keeper))) (defn search-collect-timelines-entries-task [{:keys [search-type predicate-code-str query-str val-ref] :as criteria} {:keys [print-length print-level] :or {print-level 2 print-length 10}}] (let [keeper (case search-type :pr-str (fn [thread-id tl-idx tl-entry] (when (or (expr-trace/expr-trace? tl-entry) (fn-return-trace/fn-return-trace? tl-entry)) (let [result (index-protos/get-expr-val tl-entry) pprint-val (:val-str (rt-values/val-pprint-ref result {:print-length print-length :print-level print-level :pprint? false}))] (when (str/includes? pprint-val query-str) (-> tl-entry index-protos/as-immutable (ensure-indexes tl-idx) reference-timeline-entry! (assoc :entry-preview pprint-val :thread-id thread-id)))))) :custorm-predicate #?(:clj (let [custom-pred-fn (try (eval (read-string predicate-code-str)) (catch Exception _ (constantly false)))] (fn [thread-id tl-idx tl-entry] (try (when (or (expr-trace/expr-trace? tl-entry) (fn-return-trace/fn-return-trace? tl-entry)) (let [result (index-protos/get-expr-val tl-entry)] (when (custom-pred-fn result) (let [pprint-val (:val-str (rt-values/val-pprint-ref result {:print-length 3 :print-level 3 :pprint? false}))] (-> tl-entry index-protos/as-immutable (ensure-indexes tl-idx) reference-timeline-entry! (assoc :entry-preview pprint-val :thread-id thread-id)))))) (catch Exception _ nil)))) :cljs (do #_:clj-kondo/ignore predicate-code-str identity)) :val-identity (let [search-val (deref-value val-ref)] (fn [thread-id tl-idx tl-entry] (when (or (expr-trace/expr-trace? tl-entry) (fn-return-trace/fn-return-trace? tl-entry)) (let [result (index-protos/get-expr-val tl-entry)] (when (identical? search-val result) (let [pprint-val (:val-str (rt-values/val-pprint-ref result {:print-length 3 :print-level 3 :pprint? false}))] (-> tl-entry index-protos/as-immutable (ensure-indexes tl-idx) reference-timeline-entry! (assoc :entry-preview pprint-val :thread-id thread-id)))))))))] (submit-async-interruptible-batched-timelines-keep-task (index-api/timelines-for criteria) keeper))) (def discard-flow index-api/discard-flow) (def all-flows-threads index-api/all-threads) (defn clear-flows [] (let [flows-ids (->> (all-flows-threads) (map first) (into #{}))] (doseq [fid flows-ids] (discard-flow fid)))) (defn clear-runtime-state [] (clear-flows) (rt-values/clear-vals-ref-registry) (rt-outputs/clear-outputs)) (def clear-outputs rt-outputs/clear-outputs) (def flow-threads-info index-api/flow-threads-info) (defn goto-location [flow-id {:keys [thread/id thread/idx]}] (rt-events/publish-event! (rt-events/make-goto-location-event flow-id id idx))) (defn stack-for-frame [flow-id thread-id fn-call-idx] (index-api/stack-for-frame flow-id thread-id fn-call-idx)) (defn set-recording [enable?] (tracer/set-recording enable?)) (defn set-multi-timeline-recording [enable?] (tracer/set-multi-timeline-recording enable?)) (def set-thread-trace-limit tracer/set-thread-trace-limit) (def set-heap-limit tracer/set-heap-limit) (defn toggle-recording [] (if (tracer/recording?) (tracer/stop-recording) (tracer/set-recording true))) (defn toggle-multi-timeline-recording [] (if (tracer/multi-timeline-recording?) (set-multi-timeline-recording false) (do (set-multi-timeline-recording true) (set-recording true)))) (defn switch-record-to-flow [flow-id] (tracer/set-current-flow-id flow-id)) (defn jump-to-last-expression-in-this-thread [] (let [flow-id (tracer/get-current-flow-id) thread-id (utils/get-current-thread-id) last-ex-loc (when-let [cnt (count (index-api/get-timeline flow-id thread-id))] {:thread/id thread-id :thread/name (utils/get-current-thread-name) :thread/idx (dec cnt)})] (if last-ex-loc (goto-location flow-id last-ex-loc) (log "No recordings for this thread yet")))) #?(:clj (defn unblock-thread [thread-id] (tracer/unblock-thread thread-id))) #?(:clj (defn unblock-all-threads [] (tracer/unblock-all-threads))) #?(:clj (defn add-breakpoint! ([fq-fn-symb opts] (add-breakpoint! fq-fn-symb opts (constantly true))) ([fq-fn-symb {:keys [disable-events?]} args-pred] (tracer/add-breakpoint! (namespace fq-fn-symb) (name fq-fn-symb) args-pred) (when-not disable-events? (rt-events/publish-event! (rt-events/make-break-installed-event fq-fn-symb)))))) #?(:clj (defn remove-breakpoint! [fq-fn-symb {:keys [disable-events?]}] (tracer/remove-breakpoint! (namespace fq-fn-symb) (name fq-fn-symb)) (when-not disable-events? (rt-events/publish-event! (rt-events/make-break-removed-event fq-fn-symb))))) #?(:clj (defn clear-breakpoints! [] (let [brks (tracer/all-breakpoints)] (tracer/clear-breakpoints!) (doseq [[fn-ns fn-name] brks] (rt-events/publish-event! (rt-events/make-break-removed-event (symbol fn-ns fn-name))))))) (defn ping [] :pong) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; To be used form the repl connections ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; #?(:clj (defn publish-event! [kind ev config] ;; do it on a different thread since we ;; don't want to block for event publication (future (case kind :clj (rt-events/publish-event! ev) ;; sending a event for ClojureScript from the Clojure (shadow) side is kind of ;; hacky. We evaluate the publish-event! expresion in the cljs runtime. :cljs (hansel-inst-utils/eval-in-ns-fn-cljs 'cljs-user `(flow-storm.runtime.events/publish-event! ~ev) config))) nil)) #?(:clj (defn vanilla-instrument-var [kind var-symb {:keys [disable-events?] :as config}] (let [inst-fn (case kind :clj hansel/instrument-var-clj :cljs hansel/instrument-var-shadow-cljs)] (log (format "Instrumenting var %s %s" var-symb config)) (inst-fn var-symb (merge (tracer/hansel-config config) config)) (when-not disable-events? (publish-event! kind (rt-events/make-vanilla-var-instrumented-event (name var-symb) (namespace var-symb)) config))))) #?(:clj (defn vanilla-uninstrument-var [kind var-symb {:keys [disable-events?] :as config}] (let [inst-fn (case kind :clj hansel/uninstrument-var-clj :cljs hansel/uninstrument-var-shadow-cljs)] (log (format "Uninstrumenting var %s %s" var-symb config)) (inst-fn var-symb (merge (tracer/hansel-config config) config)) (when-not disable-events? (publish-event! kind (rt-events/make-vanilla-var-uninstrumented-event (name var-symb) (namespace var-symb)) config))))) #?(:clj (defn vanilla-instrument-namespaces [kind ns-prefixes {:keys [disable-events?] :as config}] (log (format "Instrumenting namespaces %s" (pr-str ns-prefixes))) (let [inst-fn (case kind :clj hansel/instrument-namespaces-clj :cljs hansel/instrument-namespaces-shadow-cljs) {:keys [affected-namespaces]} (inst-fn ns-prefixes (merge (tracer/hansel-config config) config))] (when-not disable-events? (doseq [ns-symb affected-namespaces] (publish-event! kind (rt-events/make-vanilla-ns-instrumented-event (name ns-symb)) config)))))) #?(:clj (defn vanilla-uninstrument-namespaces [kind ns-prefixes {:keys [disable-events?] :as config}] (log (format "Uninstrumenting namespaces %s" (pr-str ns-prefixes))) (let [inst-fn (case kind :clj hansel/uninstrument-namespaces-clj :cljs hansel/uninstrument-namespaces-shadow-cljs) {:keys [affected-namespaces]} (inst-fn ns-prefixes (merge (tracer/hansel-config config) config))] (when-not disable-events? (doseq [ns-symb affected-namespaces] (publish-event! kind (rt-events/make-vanilla-ns-uninstrumented-event (name ns-symb)) config)))))) #?(:clj (defn modify-storm-instrumentation [kind {:keys [inst-kind op prefix regex]} {:keys [disable-events?] :as config}] (case kind :clj (let [[method args] (cond (and (= inst-kind :inst-only-prefix) (= op :add)) ["addInstrumentationOnlyPrefix" [prefix]] (and (= inst-kind :inst-skip-prefix) (= op :add)) ["addInstrumentationSkipPrefix" [prefix]] (and (= inst-kind :inst-skip-regex) (= op :set)) ["setInstrumentationSkipRegex" [regex]] (and (= inst-kind :inst-only-prefix) (= op :rm)) ["removeInstrumentationOnlyPrefix" [prefix]] (and (= inst-kind :inst-skip-prefix) (= op :rm)) ["removeInstrumentationSkipPrefix" [prefix]] (and (= inst-kind :inst-skip-regex) (= op :rm)) ["removeInstrumentationSkipRegex" []])] (utils/call-jvm-method "clojure.storm.Emitter" method args)) :cljs (cond (and (= inst-kind :inst-only-prefix) (= op :add)) ((requiring-resolve 'cljs.storm.api/add-instr-only-prefix) prefix) (and (= inst-kind :inst-skip-prefix) (= op :add)) ((requiring-resolve 'cljs.storm.api/add-instr-skip-prefix) prefix) (and (= inst-kind :inst-skip-regex) (= op :set)) ((requiring-resolve 'cljs.storm.api/set-instr-skip-regex) regex) (and (= inst-kind :inst-only-prefix) (= op :rm)) ((requiring-resolve 'cljs.storm.api/rm-instr-only-prefix) prefix) (and (= inst-kind :inst-skip-prefix) (= op :rm)) ((requiring-resolve 'cljs.storm.api/rm-instr-skip-prefix) prefix) (and (= inst-kind :inst-skip-regex) (= op :rm)) ((requiring-resolve 'cljs.storm.api/rm-instr-skip-regex)))) (when-not disable-events? (publish-event! kind (rt-events/make-storm-instrumentation-updated-event (get-storm-instrumentation kind)) config)))) #?(:clj (defn all-namespaces ([kind] (all-namespaces kind nil)) ([kind build-id] (case kind :clj (mapv (comp str ns-name) (all-ns)) :cljs (hansel-inst-utils/cljs-get-all-ns build-id))))) #?(:clj (defn all-vars-for-namespace ([kind ns-name] (all-vars-for-namespace kind ns-name nil)) ([kind ns-name build-id] (case kind :clj (->> (ns-interns (symbol ns-name)) keys (mapv str)) :cljs (hansel-inst-utils/cljs-get-ns-interns (symbol ns-name) build-id))))) #?(:clj (defn get-var-meta ([kind var-symb] (get-var-meta kind var-symb {})) ([kind var-symb {:keys [build-id]}] (case kind :clj (-> (meta (find-var var-symb)) (update :ns (comp str ns-name))) :cljs (hansel-inst-utils/eval-in-ns-fn-cljs 'cljs.user `(meta (var ~var-symb)) {:build-id build-id}))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Utils for calling by name, used by the websocket api calls ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def *api-fn-registry (atom {:runtime-config runtime-config :val-pprint val-pprint :data-window-push-val-data data-window-push-val-data :get-form get-form :timeline-count timeline-count :timeline-entry timeline-entry :multi-thread-timeline-count multi-thread-timeline-count :frame-data frame-data :bindings bindings :callstack-tree-root-node callstack-tree-root-node :callstack-node-childs callstack-node-childs :callstack-node-frame callstack-node-frame :fn-call-stats fn-call-stats ;; collectors tasks :collect-fn-frames-task collect-fn-frames-task :total-order-timeline-task total-order-timeline-task :thread-prints-task thread-prints-task ;; finders tasks :search-collect-timelines-entries-task search-collect-timelines-entries-task :find-expr-entry-task find-expr-entry-task :find-fn-call-task find-fn-call-task :discard-flow discard-flow :tap-value tap-value :interrupt-all-tasks interrupt-all-tasks :start-task start-task :clear-runtime-state clear-runtime-state :clear-outputs clear-outputs :flow-threads-info flow-threads-info :all-flows-threads all-flows-threads :stack-for-frame stack-for-frame :toggle-recording toggle-recording :toggle-multi-timeline-recording toggle-multi-timeline-recording :switch-record-to-flow switch-record-to-flow :set-thread-trace-limit set-thread-trace-limit :set-heap-limit set-heap-limit :ping ping #?@(:clj [:def-value def-value :all-namespaces all-namespaces :all-vars-for-namespace all-vars-for-namespace :get-var-meta get-var-meta :vanilla-instrument-var vanilla-instrument-var :vanilla-uninstrument-var vanilla-uninstrument-var :vanilla-instrument-namespaces vanilla-instrument-namespaces :vanilla-uninstrument-namespaces vanilla-uninstrument-namespaces ;; these are about configuring the prefixes :get-storm-instrumentation get-storm-instrumentation :modify-storm-instrumentation modify-storm-instrumentation ;; these are about turning on/off :storm-instrumentation-enable? storm-instrumentation-enable? :turn-storm-instrumentation turn-storm-instrumentation :unblock-thread unblock-thread :unblock-all-threads unblock-all-threads :add-breakpoint! add-breakpoint! :remove-breakpoint! remove-breakpoint!])})) (defn register-api-function [fn-key f] (swap! *api-fn-registry assoc fn-key f)) (defn api-fn-by-key [fn-key] (get @*api-fn-registry fn-key)) (defn call-api-by-fn-key [fn-key args] (let [f (api-fn-by-key fn-key)] (apply f args))) (defn runtime-started? [] (index-api/indexes-started?)) #?(:clj (defn start-runtime "Start the runtime. Will not do anything if the runtime has been already started." [] ;; NOTE: The order here is important until we replace this code with ;; better component state management (if (runtime-started?) (log "Runtime already started, skipping ...") (let [_ (log "Starting up runtime") fn-call-limits (utils/parse-thread-fn-call-limits (System/getProperty "flowstorm.threadFnCallLimits")) thread-trace-limit (when-let [limit-prop (System/getProperty "flowstorm.threadTraceLimit")] (utils/parse-int limit-prop)) heap-limit (when-let [limit-prop (System/getProperty "flowstorm.heapLimit")] (utils/parse-int limit-prop)) break-on-limit? (boolean (System/getProperty "flowstorm.throwOnLimit"))] (when thread-trace-limit (log (utils/format "Limiting threads trace count to %d. Throw on limit %s?" thread-trace-limit break-on-limit?)) (set-thread-trace-limit {:limit thread-trace-limit :break? break-on-limit?})) (when heap-limit (log (utils/format "Setting a heap limit of %d MBs" heap-limit)) (set-heap-limit {:limit heap-limit :break? break-on-limit?})) (tracer/set-recording (if (= (System/getProperty "flowstorm.startRecording") "true") true false)) (doseq [[fn-ns fn-name l] fn-call-limits] (index-api/add-fn-call-limit fn-ns fn-name l)) (index-api/start) (rt-values/clear-vals-ref-registry) (rt-outputs/setup-tap!) (jobs/run-jobs) (log "Runtime started")))) :cljs (defn start-runtime "This is meant to be called by preloads to initialize the runtime side of things. Will not do anything if the runtime has been already started." [] (if (runtime-started?) (log "Runtime already started, skipping ...") (do (log "Starting up runtime") (index-api/start) (let [recording? (if (= (env-prop "flowstorm.startRecording") "true") true false)] (tracer/set-recording recording?) (log "Recording set to " recording?)) (let [fn-call-limits (utils/parse-thread-fn-call-limits (env-prop "flowstorm.threadFnCallLimits"))] (doseq [[fn-ns fn-name l] fn-call-limits] (index-api/add-fn-call-limit fn-ns fn-name l))) (when-let [limit-prop (env-prop "flowstorm.threadTraceLimit")] (let [thread-trace-limit (utils/parse-int limit-prop) break? (boolean (env-prop "flowstorm.throwOnLimit"))] (when thread-trace-limit (log (utils/format "Limiting threads trace count to %d. Throw on limit %s?" thread-trace-limit break?)) (set-thread-trace-limit {:limit thread-trace-limit :break? break?})))) (rt-values/clear-vals-ref-registry) (rt-outputs/setup-tap!) (jobs/run-jobs) (log "Runtime started"))))) (defn setup-runtime [] (log "WARNING : setup-runtime was deprecated. If you are defining your own preloads please call (start-runtime) instead.") (start-runtime)) (defn stop-runtime [] (interrupt-all-tasks) (jobs/stop-jobs) (rt-outputs/remove-tap!) (rt-outputs/clear-outputs) (rt-values/clear-vals-ref-registry) (rt-events/clear-pending-events!) (index-api/clear-fn-call-limits) (remote-websocket-client/stop-remote-websocket-client) (tracer/clear-breakpoints!) (index-api/stop)) #?(:clj (defn remote-connect [config] (start-runtime) ;; connect to the remote websocket (remote-websocket-client/start-remote-websocket-client (assoc config :api-call-fn call-api-by-fn-key :on-connected (fn [] (let [enqueue-event! (fn [ev] (-> [:event ev] serializer/serialize remote-websocket-client/send))] (log "Connected to remote websocket") (rt-events/set-dispatch-fn enqueue-event!) (log "Remote Clojure runtime initialized")))))) :cljs ;;---------------------------------------------------------------------------------------------- (defn remote-connect [config] ;; connect to the remote websocket (try (remote-websocket-client/start-remote-websocket-client (assoc config :api-call-fn call-api-by-fn-key :on-connected (fn [] ;; subscribe and automatically push all events thru the websocket ;; if there were any events waiting to be dispatched (rt-events/set-dispatch-fn (fn [ev] (-> [:event ev] serializer/serialize remote-websocket-client/send))) (log "Debugger connection ready. Events dispatch function set and pending events pushed.")))) (catch :default e (utils/log-error "Couldn't connect to the debugger" e)))) ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Utilities for the middleware ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; These are used only by the middleware (defn all-fn-call-stats [] (reduce (fn [r [flow-id thread-id]] (let [thread-stats (index-api/fn-call-stats flow-id thread-id)] (reduce (fn [rr {:keys [fn-ns fn-name cnt]}] (update rr (str (symbol fn-ns fn-name)) #(+ (or % 0) cnt))) r thread-stats))) {} (index-api/all-threads))) (defn find-fn-call-sync [flow-id fq-fn-call-symb from-idx backward] (let [timelines (index-api/timelines-for {:flow-id flow-id}) fn-target-ns (namespace fq-fn-call-symb) fn-target-name (name fq-fn-call-symb) next-fn (if backward dec inc)] (some (fn [tl] (let [from-idx (or from-idx (if backward (dec (count tl)) 0))] (loop [i from-idx] (when (<= 0 i (dec (count tl))) (let [entry (get tl i)] (if (and (index-api/fn-call-trace? entry) (= fn-target-ns (index-api/get-fn-ns entry)) (= fn-target-name (index-api/get-fn-name entry))) (-> entry index-api/as-immutable (ensure-indexes i) (assoc :flow-id (index-protos/flow-id tl) :thread-id (index-protos/thread-id tl i)) reference-timeline-entry!) (recur (next-fn i)))))))) timelines))) ================================================ FILE: src-inst/flow_storm/runtime/events.cljc ================================================ (ns flow-storm.runtime.events) (defonce *dispatch (atom nil)) (defonce pending-events (atom [])) (defn clear-dispatch-fn! [] (reset! *dispatch nil)) (defn clear-pending-events! [] (reset! pending-events [])) (defn set-dispatch-fn [dispatch-fn] (reset! *dispatch dispatch-fn) (locking pending-events (doseq [pe @pending-events] (dispatch-fn pe)))) (defn make-flow-created-event [flow-id form-ns form timestamp] [:flow-created {:flow-id flow-id :form-ns form-ns :form (pr-str form) :timestamp timestamp}]) (defn make-flow-discarded-event [flow-id] [:flow-discarded {:flow-id flow-id}]) (defn make-threads-updated-event [flow-id flow-threads-info] [:threads-updated {:flow-id flow-id :flow-threads-info flow-threads-info}]) (defn make-timeline-updated-event [flow-id thread-id] [:timeline-updated {:flow-id flow-id :thread-id thread-id}]) (defn make-storm-instrumentation-updated-event [inst-data] [:storm-instrumentation-updated-event inst-data]) (defn make-vanilla-ns-instrumented-event [ns-name] [:vanilla-namespace-instrumented {:ns-name ns-name}]) (defn make-vanilla-ns-uninstrumented-event [ns-name] [:vanilla-namespace-uninstrumented {:ns-name ns-name}]) (defn make-vanilla-var-instrumented-event [var-name var-ns] [:vanilla-var-instrumented {:var-name var-name :var-ns var-ns}]) (defn make-vanilla-var-uninstrumented-event [var-name var-ns] [:vanilla-var-uninstrumented {:var-name var-name :var-ns var-ns}]) (defn make-tap-event [tap-val] [:tap {:value tap-val}]) (defn make-task-submitted-event [task-id] [:task-submitted {:task-id task-id}]) (defn make-task-progress-event [task-id task-data] [:task-progress (assoc task-data :task-id task-id)]) (defn make-task-finished-event ([task-id] (make-task-finished-event task-id nil)) ([task-id result] [:task-finished (cond-> {:task-id task-id} result (assoc :result result))])) (defn make-task-failed-event [task-id message] [:task-failed {:task-id task-id :message message}]) (defn make-heap-info-update-event [heap-info] [:heap-info-update heap-info]) (defn make-goto-location-event [flow-id thread-id idx] [:goto-location {:flow-id flow-id :thread-id thread-id :idx idx}]) (defn make-break-installed-event [fq-fn-symb] [:break-installed {:fq-fn-symb fq-fn-symb}]) (defn make-break-removed-event [fq-fn-symb] [:break-removed {:fq-fn-symb fq-fn-symb}]) (defn make-recording-updated-event [recording?] [:recording-updated {:recording? recording?}]) (defn make-multi-timeline-recording-updated-event [recording?] [:multi-timeline-recording-updated {:recording? recording?}]) (defn make-function-unwinded-event [ev-data] [:function-unwinded-event ev-data]) (defn make-expression-bookmark-event [ev-data] [:expression-bookmark-event ev-data]) (defn show-doc-event [vsymb] [:show-doc {:var-symbol vsymb}]) (defn make-data-window-push-val-data-event [dw-id vdata extras] [:data-window-push-val-data {:dw-id dw-id :val-data vdata :extras extras}]) (defn make-data-window-update-event [dw-id data] [:data-window-update {:dw-id dw-id :data data}]) (defn make-out-write-event [s] [:out-write {:msg s}]) (defn make-err-write-event [s] [:err-write {:msg s}]) (defn make-last-evals-update-event [last-evals-refs] [:last-evals-update {:last-evals-refs last-evals-refs}]) (defn publish-event! [[ev-key :as ev]] (if-let [dispatch @*dispatch] (dispatch ev) (when-not (#{:heap-info-update} ev-key ) (locking pending-events (swap! pending-events conj ev))))) ================================================ FILE: src-inst/flow_storm/runtime/indexes/api.cljc ================================================ (ns flow-storm.runtime.indexes.api "You can use this namespace to work with your recordings from the repl. Find more documentation on the docstrings of each specific function. From the UI you can retrieve the flow-id and thread-id of your recordings which you will need for accessing them from the repl. TIMELINES --------- Retrieving a timeline by flow-id and thread id. If you don't provide the flow-id 0 is assumed. (def timeline (get-timeline 18)) (def timeline (get-timeline 0 18)) The timeline implements many of the Clojure basic interfaces, so you can : (count timeline) (take 10 timeline) (get timeline 0) The easiest way to take a look at a thread timeline is with some code like this : (->> timeline (take 10) (mapv as-immutable)) Converting all entries to immutable values is very practical since each entry will become a Clojure map, but it is slower and consumes more memory, which becomes a thing in very long timelines, so there are functions to deal with entries objects, without having to create a map out of them. Timelines entries are of 4 different kinds: FnCallTrace, FnReturnTrace, FnUnwindTrace and ExprTrace. You can access their data by using the following functions depending on the entry : All kinds : - `as-immutable` ` ExprTrace, FnReturnTrace and FnUnwindTrace : - `get-coord-vec` - `fn-call-idx` ExprTrace, FnReturnTrace : - `get-expr-val` FnUnwindTrace : - `get-throwable` FnCallTrace : - `get-fn-name` - `get-fn-ns` - `get-fn-args` - `get-fn-parent-idx` - `get-fn-ret-idx` - `get-fn-bindings` - `get-form-id` FnBind : - `get-bind-sym-name` - `get-bind-val` You can also access the timeline as a tree by calling : - `callstack-root-node` - `callstack-node-childs` - `callstack-node-frame-data` FORMS ----- You can retrieve forms with : - `get-form` - `get-sub-form` MULTI-THREAD TIMELINES ---------------------- If you have recorded a multi-thread timeline on flow 0, you can retrieve with `total-order-timeline` like this : (def mt-timeline (total-order-timeline 0)) which you can iterate using normal Clojure functions (map, filter, reduce, get, etc). The easiest way to explore it is with some code like this : (->> mt-timeline (take 10) (map as-immutable)) Each total order timeline entry object can give you a pointer to the entry on its thread timeline and the thread id via : - `tote-entry` - `tote-thread-id` OTHER UTILITIES --------------- - `stack-for-frame` - `fn-call-stats` - `find-expr-entry` - `find-fn-call-entry` - `print-threads` - `get-fn-call` - `get-sub-form-at-coord` - `get-sub-form` - `find-entry-by-sub-form-pred` - `find-entry-by-sub-form-pred-all-threads` - `fn-call-trace?` - `expr-trace?` - `fn-return-trace?` - `fn-unwind-trace?` - `fn-end-trace?` " (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.runtime.indexes.timeline-index :as timeline-index :refer [ensure-indexes]] [flow-storm.runtime.values :as rt-values] [flow-storm.runtime.events :as events] [flow-storm.runtime.indexes.thread-registry :as thread-registry] [flow-storm.runtime.indexes.form-registry :as form-registry] [flow-storm.runtime.types.fn-call-trace :as fn-call-trace] [flow-storm.runtime.types.fn-return-trace :as fn-return-trace] [flow-storm.runtime.types.expr-trace :as expr-trace] [flow-storm.runtime.indexes.total-order-timeline :as total-order-timeline] [flow-storm.utils :as utils] [hansel.utils :as hansel-utils] [clojure.pprint :as pp] [clojure.string :as str] [clojure.set :as set] #?(:clj [clojure.core.protocols :as cp]))) (declare discard-flow) ;; Registry that contains all flows and threads timelines. ;; It is an instance of `flow-storm.runtime.indexes.thread-registry/ThreadRegistry`. (defonce flow-thread-registry nil) ;; Registry that contains all registered forms. ;; It could be anything that implements `flow-storm.runtime.indexes.protocols/FormRegistryP` ;; ;; Currently it can be an instance of `flow-storm.runtime.indexes.thread-registry/FormRegistry` ;; or `clojure.storm/FormRegistry` when working with ClojureStorm. (defonce forms-registry nil) ;; Stores the function calls limits for different functions. (defonce fn-call-limits (atom nil)) (defn add-fn-call-limit [fn-ns fn-name limit] (swap! fn-call-limits assoc-in [fn-ns fn-name] limit)) (defn rm-fn-call-limit [fn-ns fn-name] (swap! fn-call-limits update fn-ns dissoc fn-name)) (defn clear-fn-call-limits [] (reset! fn-call-limits nil)) (defn get-fn-call-limits [] @fn-call-limits) (defn indexes-started? [] (not (nil? flow-thread-registry))) (defn register-form [form-data] (if forms-registry (index-protos/register-form forms-registry (:form/id form-data) form-data) (utils/log (str "Warning, trying to register a form before FlowStorm startup. If you have #trace tags on your code you will have to evaluate them again after starting the debugger." (pr-str form-data))))) (defn create-flow [{:keys [flow-id ns form timestamp]}] (discard-flow flow-id) (events/publish-event! (events/make-flow-created-event flow-id ns form timestamp))) #?(:clj (defn start [] (alter-var-root #'flow-thread-registry (fn [_] (when (utils/storm-env?) ((requiring-resolve 'flow-storm.tracer/hook-clojure-storm)) (utils/log "Storm functions plugged in")) (let [registry (thread-registry/make-flows-threads-registry)] registry))) (alter-var-root #'forms-registry (fn [_] (index-protos/start-form-registry (if (utils/storm-env?) ((requiring-resolve 'flow-storm.runtime.indexes.storm-form-registry/make-storm-form-registry)) (form-registry/make-form-registry))))) (utils/log "Runtime index system started")) :cljs (defn start [] (when-not flow-thread-registry (set! flow-thread-registry (thread-registry/make-flows-threads-registry)) (set! forms-registry (index-protos/start-form-registry (form-registry/make-form-registry))) (utils/log (str "Runtime index system started"))))) #?(:clj (defn stop [] (when (utils/storm-env?) ((requiring-resolve 'flow-storm.tracer/unhook-clojure-storm)) (utils/log "Storm functions unplugged")) (alter-var-root #'flow-thread-registry (constantly nil)) (alter-var-root #'forms-registry index-protos/stop-form-registry) (utils/log "Runtime index system stopped")) :cljs (defn stop [] (set! flow-thread-registry (constantly nil)) (set! forms-registry index-protos/stop-form-registry) (utils/log "Runtime index system stopped"))) (defn flow-exists? [flow-id] (when flow-thread-registry (index-protos/flow-exists? flow-thread-registry flow-id))) (defn check-fn-limit! "Automatically decrease the limit for the function if it exists. Returns true when there is a limit and it is reached, false otherwise." [*thread-fn-call-limits fn-ns fn-name] (when-let [fcl @*thread-fn-call-limits] (when-let [l (get-in fcl [fn-ns fn-name])] (if (not (pos? l)) true (do (swap! *thread-fn-call-limits update-in [fn-ns fn-name] dec) false))))) #?(:cljs (defn flow-threads-info [flow-id] [{:flow/id flow-id :thread/id 0 :thread/name "main" :thread/blocked? false}]) :clj (defn flow-threads-info [flow-id] (when flow-thread-registry (index-protos/flow-threads-info flow-thread-registry flow-id)))) (defn create-thread-tracker! [flow-id thread-id thread-name] (let [timeline (timeline-index/make-index flow-id thread-id thread-name) thread-tracker (index-protos/register-thread flow-thread-registry flow-id thread-id thread-name timeline @fn-call-limits)] (events/publish-event! (events/make-threads-updated-event flow-id (flow-threads-info flow-id))) thread-tracker)) (defn get-timeline ([flow-id thread-id] (when flow-thread-registry (:thread/timeline (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)))) ([thread-id] (some (fn [[fid tid]] (when (= thread-id tid) (get-timeline fid tid))) (index-protos/all-threads flow-thread-registry)))) (defn get-or-create-thread-tracker [flow-id thread-id thread-name] (when-not (flow-exists? flow-id) (create-flow {:flow-id flow-id})) (if-let [th-tracker (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] th-tracker (create-thread-tracker! flow-id thread-id thread-name))) ;;;;;;;;;;;;;;;;;;;;;;; ;; Indexes build api ;; ;;;;;;;;;;;;;;;;;;;;;;; (defn add-form-init-trace [trace] (register-form trace)) (defn add-fn-call-trace [flow-id thread-id thread-name fn-ns fn-name form-id args total-order-recording?] (let [{:thread/keys [timeline *fn-call-limits *thread-limited]} (get-or-create-thread-tracker flow-id thread-id thread-name)] (if-not @*thread-limited (if-not (check-fn-limit! *fn-call-limits fn-ns fn-name) ;; if we are not limited, go ahead and record fn-call (let [tl-idx (index-protos/add-fn-call timeline fn-ns fn-name form-id args)] (when (and tl-idx total-order-recording?) (index-protos/record-total-order-entry flow-thread-registry flow-id timeline tl-idx)) tl-idx) ;; we hitted the limit, limit the thread with depth 1 (reset! *thread-limited 1)) ;; if this thread is already limited, just increment the depth (swap! *thread-limited inc)))) (defn add-fn-return-trace [flow-id thread-id coord ret-val total-order-recording?] (when-let [{:thread/keys [timeline *thread-limited]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (if-not @*thread-limited ;; when not limited, go ahead (let [tl-idx (index-protos/add-fn-return timeline coord ret-val)] (when (and tl-idx total-order-recording?) (index-protos/record-total-order-entry flow-thread-registry flow-id timeline tl-idx)) tl-idx) ;; if we are limited decrease the limit depth or remove it when it reaches to 0 (do (swap! *thread-limited (fn [l] (when (> l 1) (dec l)))) nil)))) (defn add-fn-unwind-trace [flow-id thread-id coord throwable total-order-recording?] (when-let [{:thread/keys [timeline *thread-limited]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (if-not @*thread-limited ;; when not limited, go ahead (when-let [tl-idx (index-protos/add-fn-unwind timeline coord throwable)] (let [unwind-trace (get timeline tl-idx) fn-idx (index-protos/fn-call-idx unwind-trace) {:keys [fn-ns fn-name]} (-> (get timeline fn-idx) index-protos/as-immutable) ev (events/make-function-unwinded-event {:flow-id flow-id :thread-id thread-id :idx tl-idx :fn-ns fn-ns :fn-name fn-name :ex-type (pr-str (type throwable)) :ex-message (ex-message throwable) :ex-hash (hash throwable)})] (when (and tl-idx total-order-recording?) (index-protos/record-total-order-entry flow-thread-registry flow-id timeline tl-idx)) (events/publish-event! ev)) tl-idx) ;; if we are limited decrease the limit depth or remove it when it reaches to 0 (do (swap! *thread-limited (fn [l] (when (> l 1) (dec l)))) nil)))) (defn add-expr-exec-trace [flow-id thread-id coord expr-val total-order-recording?] (when-let [{:thread/keys [timeline *thread-limited]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (when (not @*thread-limited) (let [tl-idx (index-protos/add-expr-exec timeline coord expr-val)] (when (and tl-idx total-order-recording?) (index-protos/record-total-order-entry flow-thread-registry flow-id timeline tl-idx)) (when (and (symbol? expr-val) (= expr-val 'flow-storm/bookmark)) (let [ev (events/make-expression-bookmark-event {:flow-id flow-id :thread-id thread-id :idx tl-idx :note (:flow-storm.bookmark/note (meta expr-val))})] (events/publish-event! ev))) tl-idx)))) (defn add-bind-trace [flow-id thread-id coord symb-name symb-val] (when-let [{:thread/keys [timeline *thread-limited]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (when (not @*thread-limited) (index-protos/add-bind timeline coord symb-name symb-val)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Indexes API. This are functions meant to be called by ;; ;; debugger-api to expose indexes to debuggers or directly by users ;; ;; to query indexes from the repl. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn get-form "Given a form id returns the registered form data as a map." [form-id] (when forms-registry (try (index-protos/get-form forms-registry form-id) #?(:clj (catch Exception _ nil) :cljs (catch js/Error _ nil))))) (defn all-threads [] (when flow-thread-registry (index-protos/all-threads flow-thread-registry))) (defn all-threads-ids [flow-id] (->> (index-protos/flow-threads-info flow-thread-registry flow-id) (mapv :thread/id))) (defn all-forms [_ _] (index-protos/all-forms forms-registry)) (defn timeline-entry [flow-id thread-id idx drift] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (when timeline (timeline-index/timeline-entry timeline idx drift)))) (defn frame-data [flow-id thread-id idx opts] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (timeline-index/tree-frame-data timeline idx opts))) (defn- coord-in-scope? [scope-coord current-coord] (if (empty? scope-coord) true (and (every? true? (map = scope-coord current-coord)) (> (count current-coord) (count scope-coord))))) (defn- partition-bindings-loops [bindings] (loop [[curr-b & rest-bindings] bindings seen-symb-coords #{} partitions [] curr-part []] (if-not curr-b (cond-> partitions (seq curr-part) (conj curr-part)) (let [symb-coord [(:symbol curr-b) (:coord curr-b)]] (if (seen-symb-coords symb-coord) (recur rest-bindings #{symb-coord} (conj partitions curr-part) [curr-b]) (recur rest-bindings (conj seen-symb-coords symb-coord) partitions (conj curr-part curr-b))))))) (defn bindings [flow-id thread-id idx _] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id) {:keys [fn-call-idx] :as entry} (timeline-index/timeline-entry timeline idx :at) frame-data (timeline-index/tree-frame-data timeline fn-call-idx {:include-binds? true}) [entry-coord entry-idx] (case (:type entry) :fn-call [[] fn-call-idx] :fn-return [(:coord entry) (:idx entry)] :fn-unwind [(:coord entry) (:idx entry)] :expr [(:coord entry) (:idx entry)]) ;; Bindings calculation for a given index (current locals) is a little bit involved because of loops. ;; To account for loops, we partition all the bindings by "iterations", which are delimited by ;; a first repeating [symbol coordinate]. ;; `visible-loops-iterations` then are all iterations partitions with a first binding <= our target idx. ;; We are not interested in any iteration that starts after our idx. visible-loops-iterations (->> (partition-bindings-loops (:bindings frame-data)) (take-while (fn [part] (<= (-> part first :visible-after) entry-idx)))) ;; The last iteration will contain bindings that are visible, and bindings that are not so ;; `invisible-last-iteration` are all the [symbol coordinate] from the last visible iteration that ;; aren't still visible at idx invisible-last-iteration (->> (last visible-loops-iterations) (keep (fn [bind] (when (> (:visible-after bind) entry-idx) [(:symbol bind) (:coord bind)]))) (into #{})) ;; for our final visible-bindings we reduce all our partitions keeping only the bindings ;; with :visible-after >= to the idx, and also which coordinates are "wrapping" ;; the coordinate for the entry at idx, but not included the `invisible-last-iteration` ones. visible-bindings (reduce (fn [vbs part] (reduce (fn [vbs' bind] (if (and (coord-in-scope? (:coord bind) entry-coord) (>= entry-idx (:visible-after bind)) (not (invisible-last-iteration [(:symbol bind) (:coord bind)]))) (assoc vbs' (:symbol bind) (:value bind)) vbs')) vbs part)) {} visible-loops-iterations)] visible-bindings)) (defn callstack-root-node "Given a flow-id and thread-id return the idx of the root node which you can use to start exploring the tree by calling `callstack-node-frame-data` and `callstack-node-childs`." [flow-id thread-id] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id) idx (index-protos/tree-root-index timeline) node [flow-id thread-id idx]] node)) (defn callstack-node-frame-data "Given a flow-id, thread-id and the index of a FnCallTrace returns a map with the frame data." [flow-id thread-id fn-call-idx] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (timeline-index/tree-frame-data timeline fn-call-idx {}))) (defn callstack-node-childs "Given a flow-id, thread-id and the index of a FnCallTrace returns a vector of touples containing [flow-id thread-id idx] of all childs." [flow-id thread-id fn-call-idx] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (into [] (map (fn [idx] [flow-id thread-id idx])) (index-protos/tree-childs-indexes timeline fn-call-idx)))) (defn stack-for-frame "Given a flow-id, thread-id and the index of a FnCallTrace returns the stack for the given frame as vector of maps containing {:keys [fn-name fn-ns fn-call-idx]}." [flow-id thread-id fn-call-idx] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id) {:keys [fn-call-idx-path]} (timeline-index/tree-frame-data timeline fn-call-idx {:include-path? true})] (reduce (fn [stack fidx] (let [{:keys [fn-name fn-ns fn-call-idx form-id]} (timeline-index/tree-frame-data timeline fidx {}) {:keys [form/def-kind multimethod/dispatch-val]} (get-form form-id)] (conj stack (cond-> {:fn-name fn-name :fn-ns fn-ns :fn-call-idx fn-call-idx :form-def-kind def-kind} (= def-kind :defmethod) (assoc :dispatch-val dispatch-val))))) [] fn-call-idx-path))) (defn reset-all-threads-trees-build-stack [flow-id] (when flow-thread-registry (let [flow-threads (index-protos/flow-threads-info flow-thread-registry flow-id)] (doseq [{:keys [thread/id]} flow-threads] (when-let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id id)] (index-protos/reset-build-stack timeline)))))) (defn fn-call-stats "Given a flow-id and optionally thread-id (could be nil) returns all functions stats as a vector of maps containing {:keys [fn-ns fn-name form-id form form-def-kind dispatch-val cnt]}" [flow-id thread-id] (let [thread-ids (if thread-id [thread-id] (all-threads-ids flow-id)) timelines-stats (mapv (fn [tid] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id tid) stats-map (index-protos/all-stats timeline)] {:stats-map stats-map :thread-id tid})) thread-ids)] (persistent! (reduce (fn [stats {:keys [stats-map thread-id]}] (reduce (fn [ss [fn-call cnt]] (if-let [form (get-form (:form-id fn-call))] (let [fs (cond-> {:thread-id thread-id :fn-ns (:fn-ns fn-call) :fn-name (:fn-name fn-call) :form-id (:form-id fn-call) :form (:form/form form) :form-def-kind (:form/def-kind form) :dispatch-val (:multimethod/dispatch-val form) :cnt cnt} (:multimethod/dispatch-val form) (assoc :dispatch-val (:multimethod/dispatch-val form)))] (conj! ss fs)) ss)) stats stats-map)) (transient []) timelines-stats)))) (defn make-frame-keeper [flow-id thread-id fn-ns fn-name form-id] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (fn [tl-idx tl-entry] (when (and (fn-call-trace/fn-call-trace? tl-entry) (if form-id (= form-id (index-protos/get-form-id tl-entry)) true) (if fn-ns (= fn-ns (index-protos/get-fn-ns tl-entry)) true) (if fn-name (= fn-name (index-protos/get-fn-name tl-entry)) true)) (timeline-index/tree-frame-data timeline tl-idx {}))))) (defn find-fn-frames "Return all the FnCallTraces matching the provided criteria." ([flow-id thread-id pred] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (into [] (keep-indexed (fn [tl-idx tl-entry] (when (and (fn-call-trace/fn-call-trace? tl-entry) (pred tl-entry)) (timeline-index/tree-frame-data timeline tl-idx {})))) timeline))) ([flow-id thread-id fn-ns fn-name form-id] (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id thread-id)] (into [] (keep-indexed (make-frame-keeper flow-id thread-id fn-ns fn-name form-id)) timeline)))) (defn discard-flow [flow-id] (let [discard-keys (some->> flow-thread-registry index-protos/all-threads (filter (fn [[fid _]] (= fid flow-id))))] (when flow-thread-registry ;; this will also discard any total-order-timeline if there is one (index-protos/discard-threads flow-thread-registry discard-keys) (events/publish-event! (events/make-flow-discarded-event flow-id))))) (defn mark-thread-blocked [flow-id thread-id breakpoint] (when flow-thread-registry (index-protos/set-thread-blocked flow-thread-registry flow-id thread-id breakpoint) (events/publish-event! (events/make-threads-updated-event flow-id (flow-threads-info flow-id))))) (defn mark-thread-unblocked [flow-id thread-id] (when flow-thread-registry (index-protos/set-thread-blocked flow-thread-registry flow-id thread-id nil) (events/publish-event! (events/make-threads-updated-event flow-id (flow-threads-info flow-id))))) (defn timelines-for "Returns a collection of [flow-id thread-id timeline] that matches the optional criteria. skip-threads can be a set of thread ids." [{:keys [flow-id thread-id skip-threads]}] (->> (all-threads) (keep (fn [[fid tid]] (when (and (or (nil? flow-id) (= flow-id fid)) (or (nil? thread-id) (= thread-id tid)) (not (and (set? skip-threads) (skip-threads tid)))) (get-timeline fid tid)))))) (defn- keep-timeline-sub-range "Given a timeline and a sub range [from to] applies f like clojure.core/keep. The provided function f will be called with two args: thread-id and timeline-entry. " [timeline from to f] (when (<= 0 from to (count timeline)) (loop [i from batch-res (transient [])] (if (< i to) (let [tl-entry (get timeline i) ;; we get the thread-id for each entry to support multi-thread-timelines tl-thread-id (index-protos/thread-id timeline i)] (if-let [e (f tl-thread-id (if (total-order-timeline/total-order-timeline-entry? tl-entry) (index-protos/tote-thread-timeline-idx tl-entry) i) (if (total-order-timeline/total-order-timeline-entry? tl-entry) (let [th-timeline (index-protos/tote-thread-timeline tl-entry) th-tl-idx (index-protos/tote-thread-timeline-idx tl-entry)] (get th-timeline th-tl-idx)) tl-entry))] (recur (inc i) (conj! batch-res e)) (recur (inc i) batch-res))) (persistent! batch-res))))) (defn async-interruptible-batched-timelines-keep "Like clojure.core/keep over timelines entries but async, applying f in batches of size batch-size. The provided function f will be called with two args: thread-id and timeline-entry. Returns a map {:keys [start interrupt]} with two functions which can be used to interrupt the collection processing between batches. on-batch should be a callback of 3 args, that will be called with the flow-id thread-id and the result of processing each batch with xf. on-end will be called with no args for signaling the end." [f timelines {:keys [on-batch on-end]}] (let [batch-size 10000 interrupted? (atom false) interrupt (fn interrupt [] (reset! interrupted? true)) start (fn start [] (let [process-next-batch (fn process-next-batch [[timeline & rtimelines :as work-timelines] from-idx] (if (or @interrupted? (nil? timeline)) (on-end) ;; else keep collecting (let [to-idx (min (+ from-idx batch-size) (count timeline)) batch-res (locking timeline ;; this locking because the timeline is mutable, ;; but maybe the timeline should expose a way of iterating sub sequences (keep-timeline-sub-range timeline from-idx to-idx f))] (on-batch batch-res) (if (= to-idx (count timeline)) ;; if the batch we just processed was the last one of the timeline, ;; change timelines #?(:clj (recur rtimelines 0) :cljs (js/setTimeout process-next-batch 0 rtimelines 0)) ;; else keep collecting from the current timeline #?(:clj (recur work-timelines to-idx) :cljs (js/setTimeout process-next-batch 0 work-timelines to-idx))))))] (process-next-batch timelines 0)))] {:interrupt interrupt :start start})) (defn- find-in-timeline-sub-range "Given a timeline tries to find a entry using pred but only between from-idx to to-idx. It can also search backwards. pred should be a fn of three args (fn [form-id idx entry]) that if matches should return the entry. When needs-form-id? is false (default) form-id will be nil. Done for perf reasons. If the pred matches, returns the immutable version of the entry." [timeline from-idx to-idx pred {:keys [backward? needs-form-id?]}] (let [next-idx (if backward? dec inc)] ;; this locking because the timeline is mutable, ;; but maybe the timeline should expose a way of iterating sub sequences (locking timeline (loop [idx from-idx] (when-not (= idx to-idx) (let [entry (get timeline idx) form-id (when needs-form-id? (let [fn-call (if (fn-call-trace/fn-call-trace? entry) entry (get timeline (index-protos/fn-call-idx entry)))] (index-protos/get-form-id fn-call)))] (if-let [r (pred form-id idx entry)] (-> r index-protos/as-immutable (ensure-indexes idx)) (recur (next-idx idx))))))))) (defn timelines-async-interruptible-find-entry "Search all timelines entries with pred. If flow-id and thread-id are provided, search will only be restricted to that thread timeline. pred should be a function of three arguments form-id, idx and the entry that if matches should return the entry. form-id will be nil by default unless needs-form-id? is true. from-idx can be used to start the search from its position. backward? will change the direction of the search. skip-threads is an optional set of threds ids you would like to skip the search on. This funciton returns immediately and you should provide the following callbacks : - on-progress [optional], a fn of one arg containing a 0-100 integer of the progress - on-match, a fn of one arg called with a map containing the matched entry info - on-end, a fn of zero args that will be called for signaling that the search reached the end with no matches Returns a map of {:keys [start interrupt]}, two functions of zero args you should call to start the process or interrupt it while it is running." [pred timelines {:keys [from-idx backward? needs-form-id?]} {:keys [on-progress on-match on-end]}] (let [batch-size 10000 total-batches (max 1 (->> (all-threads) (mapv (fn [[fid tid]] (quot (count (get-timeline fid tid)) batch-size))) (reduce +))) batches-processed (volatile! 0) interrupted? (atom false) interrupt (fn [] (reset! interrupted? true)) start (fn [] (let [find-next-batch (fn find-next-batch [[timeline & rtimelines :as work-timelines] curr-idx] (if (or @interrupted? (nil? timeline)) (on-end) ;; else keep searching (let [curr-idx (or curr-idx (if backward? (dec (count timeline)) 0)) flow-id (index-protos/flow-id timeline) thread-id (index-protos/thread-id timeline curr-idx) to-idx (if backward? (max (- curr-idx batch-size) 0) (min (+ curr-idx batch-size) (count timeline))) entry (find-in-timeline-sub-range timeline curr-idx to-idx pred {:backward? backward? :needs-form-id? needs-form-id?})] (if entry ;; if we found the entry report the match and finish (on-match (assoc entry :flow-id flow-id :thread-id thread-id)) ;; else report progress and continue searching (do (when on-progress (on-progress (int (* 100 (/ @batches-processed total-batches))))) (if (or (and (not backward?) (= to-idx (count timeline))) (and backward? (= to-idx 0))) ;; if the batch we just searched was the last one of the timeline, ;; change timelines #?(:clj (recur rtimelines from-idx) :cljs (js/setTimeout find-next-batch 0 rtimelines from-idx)) ;; else keep collecting from the current timeline #?(:clj (recur work-timelines to-idx) :cljs (js/setTimeout find-next-batch 0 work-timelines to-idx))))))) )] (find-next-batch timelines from-idx)))] {:interrupt interrupt :start start})) (defn find-flow-fn-call [flow-id] (some (fn [[fid tid]] (when (= flow-id fid) (let [{:thread/keys [timeline]} (index-protos/get-thread-tracker flow-thread-registry flow-id tid)] (when (pos? (count timeline)) (-> timeline first index-protos/as-immutable (ensure-indexes 0) (assoc :flow-id fid :thread-id tid)) )))) (index-protos/all-threads flow-thread-registry))) (defn build-find-fn-call-entry-predicate [{:keys [fn-ns fn-name form-id args-pred]}] (fn [entry-form-id _ tl-entry] (when (and (fn-call-trace/fn-call-trace? tl-entry) (if form-id (= form-id entry-form-id) true) (if fn-ns (= (index-protos/get-fn-ns tl-entry) fn-ns) true) (if fn-name (= (index-protos/get-fn-name tl-entry) fn-name) true) (if args-pred (args-pred (index-protos/get-fn-args tl-entry)) true)) tl-entry))) #?(:clj (defn find-fn-call-entry "Find the first match of a FnCallTrace entry that matches the criteria. Criteria (can be combined in any way) : - flow-id, if not present will match any. - thread-id, if not present will match any. - from-idx, where to start searching, defaults to: 0 or last when backward? is true. - backward?, search backwards, default to false. - fn-ns, the function namespace to match. - fn-name, the function name to match. - args-pred, a predicate of one argument that will receive the args vector. Absent criteria that doesn't have a default value will always match." [criteria] (let [result-prom (promise) {:keys [start]} (timelines-async-interruptible-find-entry (build-find-fn-call-entry-predicate criteria) (timelines-for criteria) criteria {:on-match (fn [m] (deliver result-prom m)) :on-end (fn [] (deliver result-prom nil))})] (start) @result-prom))) (defn- entry-matches-file-and-line? [entry-form-id entry file line] (let [form (get-form entry-form-id)] (when (= file (:form/file form)) (let [coord (index-protos/get-coord-vec entry) sub-form (hansel-utils/get-form-at-coord (:form/form form) coord)] (-> sub-form meta :line (= line)))))) (defn build-find-expr-entry-predicate [{:keys [identity-val equality-val custom-pred-form coord form-id file line] :as criteria}] (let [coord (when coord (utils/stringify-coord coord)) custom-pred-fn #?(:clj (when custom-pred-form (eval (read-string custom-pred-form))) :cljs (do (utils/log (str "Custom stepping is not supported in ClojureScript yet " custom-pred-form)) (constantly true)))] (fn [entry-form-id _ tl-entry] (when (and (or (fn-return-trace/fn-return-trace? tl-entry) (expr-trace/expr-trace? tl-entry)) (not (identical? (index-protos/get-expr-val tl-entry) :flow-storm.power-step/skip)) (if (contains? criteria :identity-val) (identical? (index-protos/get-expr-val tl-entry) identity-val) true) (if (contains? criteria :equality-val) (= (index-protos/get-expr-val tl-entry) equality-val) true) (if coord (= coord (index-protos/get-coord-raw tl-entry)) true) (if form-id (= form-id entry-form-id) true) (if (and file line) (entry-matches-file-and-line? entry-form-id tl-entry file line) true) (if custom-pred-fn (custom-pred-fn (index-protos/get-expr-val tl-entry)) true)) tl-entry)))) #?(:clj (defn find-expr-entry "Find the first match of a ExprTrace or ReturnTrace entry that matches criteria. Criteria (can be combined in any way) : - flow-id, if not present will match any. - thread-id, if not present will match any. - from-idx, where to start searching, defaults to: 0 or last when backward? is true. - backward?, search backwards, default to false. - identity-val, search this val with identical? over expressions values. - equality-val, search this val with = over expressions values. - form-id, search by the form-id of the expression. - file and line, search by file name and line - coord, a vector with a coordinate to match, like [3 1 2]. - custom-pred-form, a string with a form to use as a custom predicate over expression values, like \"(fn [v] (map? v))\" - skip-threads, a set of threads ids to skip. Absent criteria that doesn't have a default value will always match. " [criteria] (let [result-prom (promise) {:keys [start]} (timelines-async-interruptible-find-entry (build-find-expr-entry-predicate criteria) (timelines-for criteria) criteria {:on-match (fn [m] (deliver result-prom m)) :on-end (fn [] (deliver result-prom nil))})] (start) @result-prom))) (defn total-order-timeline "Retrieves the total order timeline for a flow-id if there is one recorded. Look at this namespace docstring for more info." [flow-id] (index-protos/total-order-timeline flow-thread-registry flow-id)) (defn detailed-total-order-timeline [flow-id] (let [timeline (total-order-timeline flow-id) details-mapper (total-order-timeline/make-detailed-timeline-mapper forms-registry)] (into [] (map (fn [tote] (let [th-tl (index-protos/tote-thread-timeline tote) th-tl-idx (index-protos/tote-thread-timeline-idx tote) th-id (index-protos/thread-id th-tl th-tl-idx) th-entry (get th-tl th-tl-idx)] (details-mapper th-id th-tl-idx th-entry)))) timeline))) (defn make-thread-prints-keeper [printers] ;; printers is a map of {form-id {coord-vec-1 {:format-str :print-length :print-level :transform-expr-str}}} (let [printers (utils/update-values printers (fn [corrds-map] (-> corrds-map (utils/update-keys (fn [coord-vec] (let [scoord (str/join "," coord-vec)] #?(:cljs scoord :clj (.intern scoord))))) (utils/update-values (fn [printer-params] #?(:cljs printer-params :clj (let [trans-expr (:transform-expr-str printer-params)] (try (if-not (str/blank? trans-expr) (let [expr-fn (-> trans-expr read-string eval)] (assoc printer-params :transform-expr-fn expr-fn)) printer-params) (catch Exception e (utils/log-error (str "Error evaluating printer transform expresion " trans-expr) e) printer-params))))))))) maybe-print-entry (fn [form-id thread-id entry-idx tl-entry] (when (contains? printers form-id) (let [coords-map (get printers form-id) coord (index-protos/get-coord-raw tl-entry)] (when (contains? coords-map coord) ;; we are interested in this coord so lets print it (let [{:keys [print-length print-level format-str transform-expr-fn]} (get coords-map coord) transform-expr-fn (or transform-expr-fn identity) val (index-protos/get-expr-val tl-entry)] (binding [*print-length* print-length *print-level* print-level] (let [transf-expr (transform-expr-fn val)] {:text (utils/format format-str (if (string? transf-expr) ;; don't pr-str strings so we don't escape newlines transf-expr (pr-str transf-expr))) :idx entry-idx :thread-id thread-id}))))))) threads-stacks (atom {})] (fn [thread-id tl-idx tl-entry] (let [form-id (when-let [thread-fn-call (first (get @threads-stacks thread-id))] (index-protos/get-form-id thread-fn-call))] (cond (fn-call-trace/fn-call-trace? tl-entry) (do (swap! threads-stacks (fn [ths-stks] (update ths-stks thread-id conj tl-entry))) nil) (fn-return-trace/fn-end-trace? tl-entry) (let [p (maybe-print-entry form-id thread-id tl-idx tl-entry)] (swap! threads-stacks (fn [ths-stks] (update ths-stks thread-id pop))) p) (expr-trace/expr-trace? tl-entry) (maybe-print-entry form-id thread-id tl-idx tl-entry)))))) (defn timelines-mod-timestamps "Returns a set of maps, each containing the thread timeilne last-modified timestamp, which is the last time something was recorded on it." [] (reduce (fn [acc [fid tid]] (let [tl (get-timeline fid tid)] (conj acc {:flow-id fid :thread-id tid :last-modified (index-protos/last-modified tl)}))) #{} (all-threads))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Utilities for exploring indexes from the repl ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn print-threads "Prints a table with all :flow-id, :thread-id and :thread-name currently found in recordings " [] (->> (all-threads) (map #(zipmap [:flow-id :thread-id :thread-name] %)) pp/print-table)) (defn fn-call-idx "Given a ExprTrace, FnReturnTrace or FnUnwindTrace entry return the possition of the FnCallTrace entry wrapping it." [entry] (index-protos/fn-call-idx entry)) (defn get-coord-vec "Given a ExprTrace, FnReturnTrace or FnUnwindTrace return its coordinate vector." [entry] (index-protos/get-coord-vec entry)) (defn get-coord "Given a ExprTrace, FnReturnTrace or FnUnwindTrace return its coordinate string." [entry] (index-protos/get-coord-raw entry)) (defn get-expr-val "Given a ExprTrace or FnReturnTrace entry return its expression value." [entry] (index-protos/get-expr-val entry)) (defn get-throwable "Returns the throwable of a FnUnwindTrace entry." [entry] (index-protos/get-throwable entry)) (defn as-immutable "Returns a hasmap representing this timeline entry." [entry] (index-protos/as-immutable entry)) (defn get-fn-name "Returns the function name of a FnCallTrace timeline entry." [entry] (index-protos/get-fn-name entry)) (defn get-fn-ns "Returns the function namespace of a FnCallTrace timeline entry." [entry] (index-protos/get-fn-ns entry)) (defn get-fn-args "Returns the arguments of a FnCallTrace timeline entry." [entry] (index-protos/get-fn-args entry)) (defn get-fn-parent-idx "Returns the parent function index of a FnCallTrace timeline entry." [entry] (index-protos/get-parent-idx entry)) (defn get-fn-ret-idx "Given a FnCallTrace timeline entry returns the index of its matching FnReturnTrace or FnUnwindTrace entry." [entry] (index-protos/get-ret-idx entry)) (defn get-fn-bindings "Given a FnCallTrace entry return its bindings." [entry] (index-protos/bindings entry)) (defn get-form-id "Given a FnCallTrace entry return its form-id" [entry] (index-protos/get-form-id entry)) (defn get-bind-sym-name "Given a BindTrace, return its symbol name." [entry] (index-protos/get-bind-sym-name entry)) (defn get-bind-val "Given a BindTrace, return its value." [entry] (index-protos/get-bind-val entry)) (defn get-fn-call "Given a timeline and any index FnCallTrace" [timeline idx] (timeline-index/get-fn-call timeline idx)) (defn tote-thread-id "Given a entry from the total order timeline, returns the thread id of the timeline it points to." [entry] (index-protos/thread-id (index-protos/tote-thread-timeline entry) 0)) (defn tote-timeline-idx "Given a entry from the total order timeline, returns the thread timeline index of the entry it points to." [entry] (index-protos/tote-thread-timeline-idx entry)) (defn tote-entry "Given a entry from the total order timeline, returns the thread timeline entry it points to." [entry] (let [th-timeline (index-protos/tote-thread-timeline entry) th-idx (index-protos/tote-thread-timeline-idx entry)] (get th-timeline th-idx))) (defn get-sub-form-at-coord "Given a form and a coord inside it returns the sub-form at that coordinate." [form coord] (hansel-utils/get-form-at-coord form coord)) (defn get-sub-form "Given a timeline and a idx return it's sub-form" [timeline idx] (let [fn-call-entry (get-fn-call timeline idx) tl-entry (get timeline idx) form-id (index-protos/get-form-id fn-call-entry) expr-coord (when (or (expr-trace/expr-trace? tl-entry) (fn-return-trace/fn-end-trace? tl-entry)) (get-coord-vec tl-entry)) form (:form/form (get-form form-id))] (if expr-coord (get-sub-form-at-coord form expr-coord) form))) (defn get-form-at-coord [form-id coord] (let [form (:form/form (get-form form-id))] (hansel-utils/get-form-at-coord form coord))) (defn find-entry-by-sub-form-pred "Find a entry on the timeline that matches a predicate called with each entry sub-form." [timeline pred] (loop [idx 0] (when (< idx (count timeline)) (let [sub-form (get-sub-form timeline idx)] (if (pred sub-form) (get timeline idx) (recur (inc idx))))))) (defn find-entry-by-sub-form-pred-all-threads "Find a entry on all thread timelines for a flow that matches a predicate called with each entry sub-form." [flow-id pred] (some (fn [thread-id] (find-entry-by-sub-form-pred (get-timeline flow-id thread-id) pred)) (all-threads-ids flow-id))) (defn fn-call-trace? "Returns true if x is a FnCallTrace" [x] (fn-call-trace/fn-call-trace? x)) (defn expr-trace? "Returns true if x is a ExprTrace" [x] (expr-trace/expr-trace? x)) (defn fn-return-trace? "Returns true if x is a FnReturnTrace" [x] (fn-return-trace/fn-return-trace? x)) (defn fn-unwind-trace? "Returns true if x is a FnUnwindTrace" [x] (fn-return-trace/fn-unwind-trace? x)) (defn fn-end-trace? "Returns true if `x` is a `fn-return-trace?` or `fn-unwind-trace?`" [x] (fn-return-trace/fn-end-trace? x)) (defn timeline-flow-id "Given a timeline returns its flow-id" [timeline] (index-protos/flow-id timeline)) (defn timeline-thread-id "Given a timeline and a idx, returns the thread-id associated to the entry. On single thread timelines all index are going to return the same thread-id, which is not the case for total order timelines." [timeline idx] (index-protos/thread-id timeline idx)) (defn timeline-thread-name "Given a timeline and a idx, returns the thread-name associated to the entry. On single thread timelines all index are going to return the same thread-id, which is not the case for total order timelines." [timeline idx] (index-protos/thread-name timeline idx)) (defn- get-trasformed-entry-timeline [timeline f] (reify index-protos/TimelineP (flow-id [_] (index-protos/flow-id timeline)) (thread-id [_ _] (index-protos/thread-id timeline nil)) (thread-name [_ _] (index-protos/thread-name timeline nil)) index-protos/FnCallStatsP (all-stats [_] (index-protos/all-stats timeline)) index-protos/TreeP (tree-root-index [_] (index-protos/tree-root-index timeline)) (tree-childs-indexes [_ fn-call-idx] (index-protos/tree-childs-indexes timeline fn-call-idx)) #?@(:clj [clojure.lang.Counted (count [_] (count timeline)) clojure.lang.Seqable (seq [_] (map f (seq timeline))) cp/CollReduce (coll-reduce [_ f] (cp/coll-reduce timeline (fn [acc e] (f acc (f e))))) (coll-reduce [_ f v] (cp/coll-reduce timeline (fn [acc e] (f acc (f e))) v)) clojure.lang.ILookup (valAt [_ k] (f (get timeline k))) (valAt [_ k not-found] (if-let [e (get timeline k)] (f e) not-found)) clojure.lang.Indexed (nth [_ k] (f (get timeline k))) (nth [_ k not-found] (if-let [e (get timeline k)] (f e) not-found))] :cljs [ICounted (-count [_] (count timeline)) ISeqable (-seq [_] (map f (seq timeline))) IReduce (-reduce [_ f] (reduce (fn [acc e] (f acc (f e))) timeline)) (-reduce [_ f start] (reduce (fn [acc e] (f acc (f e))) start timeline)) ILookup (-lookup [_ k] (f (get timeline k))) (-lookup [_ k not-found] (if-let [e (get timeline k)] (f e) not-found)) IIndexed (-nth [_ n] (f (get timeline n))) (-nth [_ n not-found] (if-let [e (get timeline n)] (f e) not-found))]))) (defn get-full-maps-timeline [flow-id thread-id] (let [timeline (get-timeline flow-id thread-id)] (get-trasformed-entry-timeline timeline (fn [entry] (let [{:keys [form-id type fn-call-idx] :as imm-entry} (as-immutable entry) form-id (or form-id (get-form-id (get timeline fn-call-idx))) expr-coord (when (or (expr-trace/expr-trace? entry) (fn-return-trace/fn-end-trace? entry)) (get-coord-vec entry)) form (:form/form (get-form form-id)) sub-form (if expr-coord (get-sub-form-at-coord form expr-coord) form) imm-entry' (assoc imm-entry :sub-form sub-form :form form) shallow-pr (fn [v] (binding [*print-length* 5 *print-level* 5] (pr-str v)))] (case type :fn-call (assoc imm-entry' :fn-args-preview (mapv shallow-pr (:fn-args imm-entry')) :bindings (reduce (fn [bs b] (assoc bs (get-bind-sym-name b) (get-bind-val b))) {} (get-fn-bindings entry))) (:expr :fn-return) (assoc imm-entry' :result-preview (shallow-pr (:result imm-entry'))) :fn-unwind (assoc imm-entry' :ex-message (ex-message (:throwable imm-entry'))))))))) (defn get-referenced-maps-timeline [flow-id thread-id] (let [timeline (get-timeline flow-id thread-id) reference-value! (fn [v] (:vid (rt-values/reference-value! v))) reference-entry! (fn [entry] (case (:type entry) :fn-call (-> entry (update :fn-args reference-value!) (set/rename-keys {:fn-args :fn-args-ref})) :fn-return (-> entry (update :result reference-value!) (set/rename-keys {:result :result-ref})) :fn-unwind (-> entry (update :throwable reference-value!) (set/rename-keys {:throwable :throwable-ref})) :expr (-> entry (update :result reference-value!) (set/rename-keys {:result :result-ref}))))] (get-trasformed-entry-timeline timeline (fn [entry] (let [{:keys [form-id type fn-call-idx] :as imm-entry} (as-immutable entry) form-id (if (= :fn-call type) form-id (get-form-id (get timeline fn-call-idx)))] (-> (assoc imm-entry :form-id form-id) reference-entry!)))))) (defn all-flows [] (reduce (fn [acc [fid tid]] (update acc fid (fnil conj []) tid)) {} (all-threads))) (comment (def tl (get-full-maps-timeline 0 70)) (take 10 tl) (get tl 10) ) ================================================ FILE: src-inst/flow_storm/runtime/indexes/form_registry.cljc ================================================ (ns flow-storm.runtime.indexes.form-registry (:require [flow-storm.runtime.indexes.protocols :as index-protos])) (defrecord FormRegistry [*registry] index-protos/FormRegistryP (register-form [_ form-id form] (swap! *registry assoc form-id form)) (all-forms [_] (vals @*registry)) (get-form [_ form-id] (get @*registry form-id)) (start-form-registry [this] this) (stop-form-registry [_])) (defn make-form-registry [] (->FormRegistry (atom {}))) ================================================ FILE: src-inst/flow_storm/runtime/indexes/protocols.cljc ================================================ (ns flow-storm.runtime.indexes.protocols) ;;;;;;;;;;;;;;;;;;;;;;;; ;; Timeline protocols ;; ;;;;;;;;;;;;;;;;;;;;;;;; (defprotocol ThreadTimelineRecorderP (add-fn-call [_ fn-ns fn-name form-id args]) (add-fn-return [_ coord ret-val]) (add-fn-unwind [_ coord throwable]) (add-expr-exec [_ coord expr-val]) (add-bind [_ coord symb-name symb-val])) (defprotocol TreeBuilderP (reset-build-stack [_])) (defprotocol TimelineP (flow-id [_]) (thread-id [_ idx]) (thread-name [_ idx])) (defprotocol TimelineEntryP (entry-type [_])) (defprotocol CoordableTimelineEntryP (get-coord-vec [_]) (get-coord-raw [_])) (defprotocol ExpressionTimelineEntryP (get-expr-val [_])) (defprotocol FnChildTimelineEntryP (fn-call-idx [_])) (defprotocol UnwindTimelineEntryP (get-throwable [_])) (defprotocol TreeP (tree-root-index [_]) (tree-childs-indexes [_ fn-call-idx])) (defprotocol ImmutableP (as-immutable [_])) (defprotocol ModifiableP (last-modified [_])) (defprotocol TotalOrderTimelineP (tot-add-entry [_ th-timeline th-idx]) (tot-clear-all [_])) (defprotocol TotalOrderTimelineEntryP (tote-thread-timeline [_]) (tote-thread-timeline-idx [_])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Function stats protocols ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defprotocol FnCallStatsP (all-stats [_])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Forms registry protocols ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defprotocol FormRegistryP (register-form [_ form-id form]) (all-forms [_]) (get-form [_ form-id]) (start-form-registry [_]) (stop-form-registry [_])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Thread registry protocols ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defprotocol FlowsThreadsRegistryP (all-threads [_]) (flow-threads-info [_ flow-id]) (set-thread-blocked [_ flow-id thread-id breakpoint]) (get-thread-tracker [_ flow-id thread-id]) (flow-exists? [_ flow-id]) (register-thread [_ flow-id thread-id thread-name timeline init-fn-call-limits]) (record-total-order-entry [_ flow-id th-timeline th-idx]) (total-order-timeline [_ flow-id]) (discard-threads [_ flow-threads-ids])) ;;;;;;;;;;;;;;;;;;;;;;; ;; Entries protocols ;; ;;;;;;;;;;;;;;;;;;;;;;; (defprotocol FnCallTraceP (get-fn-name [_]) (get-fn-ns [_]) (get-form-id [_]) (get-fn-args [_]) (get-parent-idx [_]) (set-parent-idx [_ idx]) (get-ret-idx [_]) (set-ret-idx [_ idx]) (add-binding [_ bind]) (set-idx [_ idx]) (bindings [_])) (defprotocol BindTraceP (get-bind-sym-name [_]) (get-bind-val [_])) ================================================ FILE: src-inst/flow_storm/runtime/indexes/storm_form_registry.clj ================================================ (ns flow-storm.runtime.indexes.storm-form-registry (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.utils :refer [log-error]]) (:import [clojure.storm FormRegistry])) (defrecord StormFormRegistry [] index-protos/FormRegistryP (all-forms [_] (FormRegistry/getAllForms)) (get-form [_ form-id] (if form-id (FormRegistry/getForm form-id) (log-error "ERROR : can't get form for id null"))) (start-form-registry [this] this) (stop-form-registry [_])) (defn make-storm-form-registry [] (->StormFormRegistry)) ================================================ FILE: src-inst/flow_storm/runtime/indexes/thread_registry.cljc ================================================ (ns flow-storm.runtime.indexes.thread-registry (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.runtime.indexes.utils :refer [int-map]] [flow-storm.runtime.indexes.total-order-timeline :as total-order-timeline] [clojure.string :as str]) #?(:clj (:import [clojure.data.int_map PersistentIntMap]))) (defrecord FlowsThreadsRegistry [;; atom of int-map with flow-id -> thread-id -> thread-info registry ;; atom of int-map with flow-id -> TotalOrderTimeline total-order-timelines] index-protos/FlowsThreadsRegistryP (all-threads [_] (reduce-kv (fn [all-ths fid threads] (into all-ths (mapv (fn [tid] [fid tid]) (keys threads)))) #{} @registry)) (flow-threads-info [_ flow-id] (->> (get @registry flow-id) vals (mapv (fn [tinfo] {:flow/id flow-id :thread/id (:thread/id tinfo) :thread/name (:thread/name tinfo) :thread/blocked (:thread/blocked tinfo)})))) (flow-exists? [_ flow-id] (contains? @registry flow-id)) (get-thread-tracker [_ flow-id thread-id] #?(:clj (some-> ^PersistentIntMap @registry ^PersistentIntMap (.get flow-id) ^clojure.lang.PersistentArrayMap (.get thread-id)) :cljs (some-> @registry (get flow-id) (get thread-id)))) (register-thread [this flow-id thread-id thread-name timeline init-fn-call-limits] (let [thread-tracker {:thread/id thread-id :thread/name (if (str/blank? thread-name) (str "Thread-" thread-id) thread-name) :thread/timeline timeline :thread/*fn-call-limits (atom init-fn-call-limits) :thread/*thread-limited (atom nil) :thread/blocked nil}] (when-not (index-protos/flow-exists? this flow-id) (swap! total-order-timelines assoc flow-id (total-order-timeline/make-total-order-timeline flow-id))) (swap! registry update flow-id (fn [threads] (assoc (or threads (int-map)) thread-id thread-tracker))) thread-tracker)) (set-thread-blocked [_ flow-id thread-id breakpoint] (swap! registry assoc-in [flow-id thread-id :thread/blocked] breakpoint)) (discard-threads [this flow-threads-ids] (doseq [[fid tid] flow-threads-ids] (swap! registry update fid dissoc tid)) ;; remove empty flows from the registry since flow-exist? uses it ;; kind of HACKY... (let [empty-flow-ids (reduce-kv (fn [efids fid threads-map] (if (empty? threads-map) (conj efids fid) efids)) #{} @registry)] (swap! registry (fn [flows-map] (apply dissoc flows-map empty-flow-ids)))) (doseq [[fid] flow-threads-ids] (index-protos/tot-clear-all (index-protos/total-order-timeline this fid)))) (record-total-order-entry [_ flow-id th-timeline th-idx] (-> (get @total-order-timelines flow-id) (index-protos/tot-add-entry th-timeline th-idx))) (total-order-timeline [_ flow-id] (get @total-order-timelines flow-id))) (defn make-flows-threads-registry [] (map->FlowsThreadsRegistry {:registry (atom (int-map)) :total-order-timelines (atom (int-map))})) ================================================ FILE: src-inst/flow_storm/runtime/indexes/timeline_index.cljc ================================================ (ns flow-storm.runtime.indexes.timeline-index (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.runtime.indexes.utils :as index-utils :refer [make-mutable-stack ms-peek ms-push ms-pop ms-count make-mutable-list ml-get ml-add ml-count mh-put make-mutable-hashmap mh-contains? mh-get mh->immutable-map]] [flow-storm.runtime.types.fn-call-trace :as fn-call-trace] [flow-storm.runtime.types.fn-return-trace :as fn-return-trace] [flow-storm.runtime.types.expr-trace :as expr-trace] [flow-storm.runtime.types.bind-trace :as bind-trace] [flow-storm.utils :as utils] #?(:clj [clojure.core.protocols :as cp]))) (deftype FnId #?(:clj [^int form-id fn-name fn-ns] :cljs [form-id fn-name fn-ns]) ;; if I type hint this with in, then the compiler complains on -hash that form-id is a [number int] #?@(:cljs [IHash (-hash [_] (unchecked-add-int (unchecked-multiply-int 31 form-id) (hash fn-name))) IEquiv (-equiv [this other] (and (= ^js/Number (.-form-id this) ^js/Number (.-form-id other)) (= ^js/String (.-fn-name this) ^js/String (.-fn-name other))))]) #?@(:clj [Object (hashCode [_] (unchecked-add-int (unchecked-multiply-int 31 form-id) (.hashCode ^String fn-name))) (equals [_ o] (and (= form-id ^int (.-form-id ^FnId o)) (.equals ^String fn-name ^String (.-fn-name ^FnId o))))])) (def fn-expr-limit #?(:cljs 9007199254740992 ;; MAX safe integer :clj 10000)) (def tree-root-idx -1) (defn- print-it [timeline] (utils/format "#flow-storm/timeline [flow-id: %d thread-id: %d count: %d]" (index-protos/flow-id timeline) (index-protos/thread-id timeline 0) (count timeline))) (defn ensure-indexes "Make sure all immutable entries (which we send to the UIs) will contain :idx and :fn-call-idx no matter what kind of entries they are." [{:keys [fn-call-idx] :as immutable-entry} idx] (assoc immutable-entry :idx idx :fn-call-idx (or fn-call-idx idx))) (deftype ExecutionTimelineTree [;; this timeline flow id flow-id ;; this timeline thread id tid ;; the thread name that created this timeilne tname ;; an array of FnCall, Expr, FnRet, FnUnwind timeline ;; a stack of pointers to prev FnCall build-stack ;; a hashmap of FnId -> long fn-call-stats ;; timestamp of the last fn-call addition to the timeline (currentTimeMillis) ^:unsynchronized-mutable ^long last-fn-timestamp ] index-protos/TimelineP (flow-id [_] flow-id) (thread-id [_ _] tid) (thread-name [_ _] tname) index-protos/ThreadTimelineRecorderP (add-fn-call [this fn-ns fn-name form-id args] (locking this (let [tl-idx (ml-count timeline) parent-idx (ms-peek build-stack) fn-call (fn-call-trace/make-fn-call-trace fn-ns fn-name form-id args parent-idx) fn-id (->FnId form-id fn-name fn-ns)] ;; update our build stack (ms-push build-stack tl-idx) ;; add the fn-call to the timeline (ml-add timeline fn-call) ;; add the fn-id to the fn-call-stats (if (mh-contains? fn-call-stats fn-id) (let [cnt (mh-get fn-call-stats fn-id)] (mh-put fn-call-stats fn-id (inc cnt))) (mh-put fn-call-stats fn-id 1)) ;; update last modified (set! last-fn-timestamp (long (utils/get-timestamp))) tl-idx))) (add-fn-return [this coord ret-val] (locking this ;; discard all expressions when no FnCall has been made yet (when (pos? (ms-count build-stack)) (let [curr-fn-call-idx (ms-peek build-stack) curr-fn-call (ml-get timeline curr-fn-call-idx) tl-idx (ml-count timeline) fn-ret (fn-return-trace/make-fn-return-trace coord ret-val curr-fn-call-idx)] (index-protos/set-ret-idx curr-fn-call tl-idx) (ml-add timeline fn-ret) (ms-pop build-stack) tl-idx)))) (add-fn-unwind [this coord throwable] (locking this ;; discard all expressions when no FnCall has been made yet (when (pos? (ms-count build-stack)) (let [ curr-fn-call-idx (ms-peek build-stack) curr-fn-call (ml-get timeline curr-fn-call-idx) tl-idx (ml-count timeline) fn-unwind (fn-return-trace/make-fn-unwind-trace coord throwable curr-fn-call-idx)] (index-protos/set-ret-idx curr-fn-call tl-idx) (ml-add timeline fn-unwind) (ms-pop build-stack) tl-idx)))) (add-expr-exec [this coord expr-val] (locking this ;; discard all expressions when no FnCall has been made yet (when (pos? (ms-count build-stack)) (let [tl-idx (ml-count timeline) curr-fn-call-idx (ms-peek build-stack) expr-exec (expr-trace/make-expr-trace coord expr-val curr-fn-call-idx)] (ml-add timeline expr-exec) tl-idx)))) (add-bind [this coord symb-name symb-val] ;; discard all expressions when no FnCall has been made yet (locking this (when (pos? (ms-count build-stack)) (let [curr-fn-call-idx (ms-peek build-stack) curr-fn-call (ml-get timeline curr-fn-call-idx) last-entry-idx (ml-count timeline) bind-trace (bind-trace/make-bind-trace symb-name symb-val coord last-entry-idx)] (index-protos/add-binding curr-fn-call bind-trace))))) index-protos/FnCallStatsP (all-stats [this] (locking this (reduce-kv (fn [r ^FnId fc cnt] (let [k {:form-id (.-form-id fc) :fn-name (.-fn-name fc) :fn-ns (.-fn-ns fc)}] (assoc r k cnt))) {} (mh->immutable-map fn-call-stats)))) index-protos/ModifiableP (last-modified [this] (locking this last-fn-timestamp)) index-protos/TreeBuilderP (reset-build-stack [this] (locking this (loop [stack build-stack] (when (pos? (ms-count stack)) (ms-pop stack) (recur stack))))) index-protos/TreeP (tree-root-index [_] tree-root-idx) (tree-childs-indexes [this fn-call-idx] (locking this (let [tl-cnt (count this)] (when (pos? tl-cnt) (let [start-idx (if (= fn-call-idx tree-root-idx) 0 (inc fn-call-idx)) end-idx (if (= fn-call-idx tree-root-idx) tl-cnt (let [fn-call (ml-get timeline fn-call-idx)] (or (index-protos/get-ret-idx fn-call) tl-cnt)))] (loop [i start-idx ch-indexes (transient [])] (if (= i end-idx) (persistent! ch-indexes) (let [tle (ml-get timeline i)] (if (fn-call-trace/fn-call-trace? tle) (recur (if-let [ret-idx (index-protos/get-ret-idx tle)] (inc ret-idx) ;; if we don't have a ret-idx it means this function didn't return yet ;; so we just recur with the end which will finish the loop tl-cnt) (conj! ch-indexes i)) (recur (inc i) ch-indexes)))))))))) #?@(:clj [clojure.lang.Counted (count [this] (locking this (ml-count timeline))) clojure.lang.Seqable (seq [this] (locking this (doall (seq timeline)))) cp/CollReduce (coll-reduce [this f] (locking this (cp/coll-reduce timeline f))) (coll-reduce [this f v] (locking this (cp/coll-reduce timeline f v))) clojure.lang.ILookup (valAt [this k] (locking this (ml-get timeline k))) (valAt [this k not-found] (locking this (or (ml-get timeline k) not-found))) clojure.lang.Indexed (nth [this k] (locking this (ml-get timeline k))) (nth [this k not-found] (locking this (or (ml-get timeline k) not-found)))] :cljs [ICounted (-count [_] (ml-count timeline)) ISeqable (-seq [_] (seq timeline)) IReduce (-reduce [_ f] (reduce f timeline)) (-reduce [_ f start] (reduce f start timeline)) ILookup (-lookup [_ k] (ml-get timeline k)) (-lookup [_ k not-found] (or (ml-get timeline k) not-found)) IIndexed (-nth [_ n] (ml-get timeline n)) (-nth [_ n not-found] (or (ml-get timeline n) not-found)) IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-it this)))])) #?(:clj (defmethod print-method ExecutionTimelineTree [timeline ^java.io.Writer w] (.write w ^String (print-it timeline)))) (defn make-index [flow-id thread-id thread-name] (let [build-stack (make-mutable-stack) timeline (make-mutable-list) stats (make-mutable-hashmap)] (->ExecutionTimelineTree flow-id thread-id thread-name timeline build-stack stats 0))) (defn- fn-call-exprs [timeline fn-call-idx] (locking timeline (let [tl-cnt (count timeline) fn-call (get timeline fn-call-idx) ret-idx (or (index-protos/get-ret-idx fn-call) tl-cnt)] (loop [idx (inc fn-call-idx) collected (transient [])] (if (= idx ret-idx) ;; we reached the end (persistent! collected) ;; keep collecting (let [tle (get timeline idx)] (if (expr-trace/expr-trace? tle) ;; if expr collect it (recur (inc idx) (conj! collected (-> tle index-protos/as-immutable (ensure-indexes idx)))) ;; else if fn-call, jump over (if (fn-call-trace/fn-call-trace? tle) (recur (if-let [ret-idx (index-protos/get-ret-idx tle)] (inc ret-idx) ;; if we don't have a ret-idx it means this function didn't return yet ;; so we just recur with the end which will finish the loop tl-cnt) collected) (recur (inc idx) collected))))))))) (defn- get-fn-call-idx-path "Return a path of timeline indexes from (root ... frame)" [timeline fn-call-idx] (locking timeline (loop [curr-fn-call-idx fn-call-idx fn-call-idx-path (transient [])] (if (nil? curr-fn-call-idx) (persistent! fn-call-idx-path) (recur (index-protos/get-parent-idx (get timeline curr-fn-call-idx)) (conj! fn-call-idx-path curr-fn-call-idx)))))) (defn get-fn-call-idx [timeline idx] (let [entry (get timeline idx)] (if (fn-call-trace/fn-call-trace? entry) idx (index-protos/fn-call-idx entry)))) (defn get-fn-call [timeline idx] (get timeline (get-fn-call-idx timeline idx))) (defn- timeline-next-out-idx "Given `idx` return the next index after the current call frame for the `timeline`." [timeline idx] (locking timeline (let [last-idx (dec (count timeline)) curr-fn-call (get-fn-call timeline idx) curr-fn-call-ret-idx (index-protos/get-ret-idx curr-fn-call)] (min last-idx (if curr-fn-call-ret-idx (inc curr-fn-call-ret-idx) last-idx))))) (defn- timeline-next-over-idx [timeline idx] (locking timeline (let [last-idx (dec (count timeline)) init-entry (get timeline idx) init-fn-call-idx (get-fn-call-idx timeline idx)] (if (fn-return-trace/fn-end-trace? init-entry) ;; if we are on a return just move next (inc idx) (loop [i (inc idx)] (if (>= i last-idx) idx (let [tl-entry (get timeline i) entry-fn-call-idx (get-fn-call-idx timeline i)] (if (= entry-fn-call-idx init-fn-call-idx) i (if (fn-call-trace/fn-call-trace? tl-entry) ;; this is an imporatant optimization for big timelines, ;; when moving forward, if we see a fn-call jump directly past the return (recur (if-let [ret-idx (index-protos/get-ret-idx tl-entry)] (inc ret-idx) last-idx)) (recur (inc i))))))))))) (defn- timeline-prev-over-idx [timeline idx] (locking timeline (let [init-entry (get timeline idx) init-fn-call-idx (get-fn-call-idx timeline idx)] (if (fn-call-trace/fn-call-trace? init-entry) ;; if we are on a fn-call just move prev (dec idx) (loop [i (dec idx)] (if-not (pos? i) idx (let [entry-fn-call-idx (get-fn-call-idx timeline i)] (if (= entry-fn-call-idx init-fn-call-idx) i ;; this is an important optimization for big timelines ;; when moving back sikip over entire functions instead ;; of just searching backwards one entry at a time until ;; we find our original frame (recur (dec entry-fn-call-idx)))))))))) (defn- timeline-prev-idx [timeline idx] (locking timeline (if-not (pos? idx) 0 (let [prev-tl-entry (get timeline (- idx 1))] (if (fn-call-trace/fn-call-trace? prev-tl-entry) (if (and (>= (- idx 2) 0) (get timeline (- idx 2))) ;; if there is a call right before a call then return the fn-call index, ;; so we don't miss the fn-call (- idx 1) ;; else just skip the fn-call and go directly to the prev expr or return (max 0 (- idx 2))) (- idx 1)))))) (defn- timeline-next-idx [timeline idx] (locking timeline (let [last-idx (dec (count timeline))] (if (>= idx last-idx) last-idx (let [next-tl-entry (get timeline (+ 1 idx))] (if (fn-call-trace/fn-call-trace? next-tl-entry) (if (get timeline (+ 2 idx)) ;; if there is a call right after a call then return the fn-call index, ;; so we don't miss the fn-call (+ 1 idx) ;; else just skip the fn-call and go directly to the next expr or return (+ 2 idx)) (+ 1 idx))))))) (defn timeline-entry [timeline idx drift] (locking timeline (when (pos? (count timeline)) (let [drift (or drift :at) last-idx (dec (count timeline)) idx (-> idx (max 0) (min last-idx)) ;; clamp the idx target-idx (case drift :next-out (timeline-next-out-idx timeline idx) :next-over (timeline-next-over-idx timeline idx) :prev-over (timeline-prev-over-idx timeline idx) :next (timeline-next-idx timeline idx) :prev (timeline-prev-idx timeline idx) :at idx) target-idx (-> target-idx (max 0) (min last-idx)) ;; clamp the target-idx tl-entry (get timeline target-idx)] (-> (index-protos/as-immutable tl-entry) (ensure-indexes target-idx)))))) (defn tree-frame-data [timeline fn-call-idx {:keys [include-path? include-exprs? include-binds?]}] (if (= fn-call-idx tree-root-idx) {:root? true} (locking timeline (when (pos? (count timeline)) (let [fn-call (get timeline fn-call-idx) _ (assert (fn-call-trace/fn-call-trace? fn-call) "Frame data should be called with a idx that correspond to a fn-call") fn-ret-idx (index-protos/get-ret-idx fn-call) fn-return (when fn-ret-idx (get timeline fn-ret-idx)) fr-data {:fn-ns (index-protos/get-fn-ns fn-call) :fn-name (index-protos/get-fn-name fn-call) :args-vec (index-protos/get-fn-args fn-call) :form-id (index-protos/get-form-id fn-call) :fn-call-idx fn-call-idx :idx fn-call-idx :parent-fn-call-idx (index-protos/get-parent-idx fn-call)} fr-data (cond-> fr-data (nil? fn-return) (assoc :return/kind :waiting) (fn-return-trace/fn-unwind-trace? fn-return) (assoc :return/kind :unwind :throwable (index-protos/get-throwable fn-return)) (fn-return-trace/fn-return-trace? fn-return) (assoc :return/kind :return :ret (index-protos/get-expr-val fn-return))) fr-data (if include-path? (assoc fr-data :fn-call-idx-path (get-fn-call-idx-path timeline fn-call-idx)) fr-data) fr-data (if include-exprs? (let [expressions (fn-call-exprs timeline fn-call-idx)] ;; expr-executions will contain also the fn-return at the end (assoc fr-data :expr-executions (cond-> expressions fn-return (conj (-> fn-return index-protos/as-immutable (ensure-indexes fn-ret-idx)))))) fr-data) fr-data (if include-binds? (assoc fr-data :bindings (map index-protos/as-immutable (index-protos/bindings fn-call))) fr-data)] fr-data))))) ================================================ FILE: src-inst/flow_storm/runtime/indexes/total_order_timeline.cljc ================================================ (ns flow-storm.runtime.indexes.total-order-timeline (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.runtime.indexes.timeline-index :refer [ensure-indexes]] [flow-storm.runtime.indexes.utils :refer [make-mutable-list ml-add ml-clear ml-count ml-get]] [flow-storm.runtime.types.fn-return-trace :as fn-return-trace :refer [fn-end-trace?]] [flow-storm.runtime.types.fn-call-trace :as fn-call-trace :refer [fn-call-trace?]] [flow-storm.runtime.types.expr-trace :as expr-trace :refer [expr-trace?]] [hansel.utils :as hansel-utils] [flow-storm.utils :as utils] #?(:clj [clojure.core.protocols :as cp]))) (defn- print-tote [tote] (let [th-tl (index-protos/tote-thread-timeline tote) th-idx (index-protos/tote-thread-timeline-idx tote) th-id (index-protos/thread-id th-tl th-idx)] (utils/format "#flow-storm/total-order-timeline-entry [ThreadId: %d, Idx: %d]" th-id th-idx))) (deftype TotalOrderTimelineEntry [th-timeline th-idx] index-protos/ImmutableP (as-immutable [_] (-> (get th-timeline th-idx) index-protos/as-immutable (ensure-indexes th-idx) (assoc :thread-id (index-protos/thread-id th-timeline 0)))) index-protos/TotalOrderTimelineEntryP (tote-thread-timeline [_] th-timeline) (tote-thread-timeline-idx [_] th-idx) #?@(:cljs [IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-tote this)))])) (defn total-order-timeline-entry? [x] (instance? TotalOrderTimelineEntry x)) #?(:clj (defmethod print-method TotalOrderTimelineEntry [tote ^java.io.Writer w] (.write w ^String (print-tote tote)))) (deftype TotalOrderTimeline [flow-id mt-timeline] index-protos/TimelineP (flow-id [_] flow-id) (thread-id [this idx] (locking this (let [tote (ml-get mt-timeline idx) th-tl (index-protos/tote-thread-timeline tote)] (index-protos/thread-id th-tl 0)))) index-protos/TotalOrderTimelineP (tot-add-entry [this th-timeline th-idx] (locking this (ml-add mt-timeline (TotalOrderTimelineEntry. th-timeline th-idx)))) (tot-clear-all [this] (locking this (ml-clear mt-timeline))) #?@(:clj [clojure.lang.Counted (count [this] (locking this (ml-count mt-timeline))) clojure.lang.Seqable (seq [this] (locking this (doall (seq mt-timeline)))) cp/CollReduce (coll-reduce [this f] (locking this (cp/coll-reduce mt-timeline f))) (coll-reduce [this f v] (locking this (cp/coll-reduce mt-timeline f v))) clojure.lang.ILookup (valAt [this k] (locking this (ml-get mt-timeline k))) (valAt [this k not-found] (locking this (or (ml-get mt-timeline k) not-found))) clojure.lang.Indexed (nth [this k] (locking this (ml-get mt-timeline k))) (nth [this k not-found] (locking this (or (ml-get mt-timeline k) not-found)))] :cljs [ICounted (-count [_] (ml-count mt-timeline)) ISeqable (-seq [_] (seq mt-timeline)) IReduce (-reduce [_ f] (reduce f mt-timeline)) (-reduce [_ f start] (reduce f start mt-timeline)) ILookup (-lookup [_ k] (ml-get mt-timeline k)) (-lookup [_ k not-found] (or (ml-get mt-timeline k) not-found)) IIndexed (-nth [_ n] (ml-get mt-timeline n)) (-nth [_ n not-found] (or (ml-get mt-timeline n) not-found))])) (defn make-total-order-timeline [flow-id] (TotalOrderTimeline. flow-id (make-mutable-list))) (defn make-detailed-timeline-mapper [forms-registry] (let [threads-stacks (atom {})] (fn [thread-id te-idx entry] (cond (fn-call-trace? entry) (do (swap! threads-stacks update thread-id conj entry) {:type :fn-call :thread-id thread-id :thread-timeline-idx te-idx :fn-ns (index-protos/get-fn-ns entry) :fn-name (index-protos/get-fn-name entry)}) (fn-end-trace? entry) (do (swap! threads-stacks update thread-id pop) {:type (if (fn-return-trace/fn-return-trace? entry) :fn-return :fn-unwind) :thread-id thread-id :thread-timeline-idx te-idx}) (expr-trace? entry) (let [[curr-fn-call] (get @threads-stacks thread-id) form-id (index-protos/get-form-id curr-fn-call) form-data (index-protos/get-form forms-registry form-id) coord (index-protos/get-coord-vec entry) expr-val (index-protos/get-expr-val entry)] {:type :expr-exec :thread-id thread-id :thread-timeline-idx te-idx :expr-str (binding [*print-length* 5 *print-level* 3] (pr-str (hansel-utils/get-form-at-coord (:form/form form-data) coord))) :expr-type (pr-str (type expr-val)) :expr-val-str (binding [*print-length* 3 *print-level* 2] (pr-str expr-val))}))))) ================================================ FILE: src-inst/flow_storm/runtime/indexes/utils.cljc ================================================ (ns flow-storm.runtime.indexes.utils #?(:clj (:require [clojure.data.int-map :as int-map])) #?(:clj (:import [java.util ArrayList Vector ArrayDeque HashMap] [java.util.concurrent ConcurrentHashMap]))) ;;;;;;;;;;;;;;;;;;; ;; Mutable stack ;; ;;;;;;;;;;;;;;;;;;; #?(:cljs (defn make-mutable-stack [] #js []) :clj (defn make-mutable-stack [] (ArrayDeque.))) #?(:cljs (defn ms-peek [mstack] (aget mstack (dec (.-length mstack)))) :clj (defn ms-peek [^ArrayDeque mstack] (.peek mstack))) #?(:cljs (defn ms-push [mstack elem] (.push mstack elem)) :clj (defn ms-push [^ArrayDeque mstack elem] (.push mstack elem))) #?(:cljs (defn ms-pop [mstack] (.pop mstack)) :clj (defn ms-pop [^ArrayDeque mstack] (.pop mstack))) #?(:cljs (defn ms-count [mstack] (.-length mstack)) :clj (defn ms-count [^ArrayDeque mstack] (.size mstack))) ;;;;;;;;;;;;;;;;;; ;; Mutable list ;; ;;;;;;;;;;;;;;;;;; #?(:cljs (defn make-mutable-list [] #js []) :clj (defn make-mutable-list [] (ArrayList.))) #?(:cljs (defn ml-get [mlist idx] (aget mlist idx)) :clj (defn ml-get [^ArrayList mlist idx] (.get mlist idx))) #?(:cljs (defn ml-add [mlist elem] (.push mlist elem)) :clj (defn ml-add [^ArrayList mlist elem] (.add mlist elem))) #?(:cljs (defn ml-count [mlist] (.-length mlist)) :clj (defn ml-count [^ArrayList mlist] (.size mlist))) #?(:cljs (defn ml-sub-list [mlist from to] (.slice mlist from to)) :clj (defn ml-sub-list [^ArrayList mlist from to] (.subList mlist from to))) #?(:cljs (defn ml-clear [mlist] (set! (.-length mlist) 0)) :clj (defn ml-clear [^ArrayList mlist] (.clear mlist))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Mutable concurrent list ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; #?(:cljs (defn make-concurrent-mutable-list [] #js []) :clj (defn make-concurrent-mutable-list [] (Vector.))) #?(:cljs (defn mcl-get [mlist idx] (aget mlist idx)) :clj (defn mcl-get [^Vector mlist idx] (.get mlist idx))) #?(:cljs (defn mcl-add [mlist elem] (.push mlist elem)) :clj (defn mcl-add [^Vector mlist elem] (.add mlist elem))) #?(:cljs (defn mcl-count [mlist] (.-length mlist)) :clj (defn mcl-count [^Vector mlist] (.size mlist))) #?(:cljs (defn mcl-sub-list [mlist from to] (.slice mlist from to)) :clj (defn mcl-sub-list [^Vector mlist from to] (.subList mlist from to))) #?(:cljs (defn mcl-clear [mlist] (set! (.-length mlist) 0)) :clj (defn mcl-clear [^Vector mlist] (.clear mlist))) ;;;;;;;;;;;;;;;;;;;;; ;; Mutable hashmap ;; ;;;;;;;;;;;;;;;;;;;;; #?(:clj (defn make-mutable-hashmap [] (HashMap.)) :cljs (defn make-mutable-hashmap [] (atom {}))) #?(:clj (defn mh->immutable-map [^HashMap mh] (into {} mh)) :cljs (defn mh->immutable-map [mh] @mh)) #?(:clj (defn mh-put [^HashMap mh k v] (.put mh k v)) :cljs (defn mh-put [mh k v] (swap! mh assoc k v))) #?(:clj (defn mh-contains? [^HashMap mh k] (.containsKey mh k)) :cljs (defn mh-contains? [mh k] (contains? @mh k))) #?(:clj (defn mh-get [^HashMap mh k] (.get mh k)) :cljs (defn mh-get [mh k] (get @mh k))) #?(:clj (defn mh-remove [^HashMap mh k] (.remove mh k)) :cljs (defn mh-remove [mh k] (swap! mh dissoc k))) #?(:clj (defn mh-keys [^HashMap mh] (.keySet mh)) :cljs (defn mh-keys [mh] (keys @mh))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Mutable concurrent hashmap ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; #?(:clj (defn make-mutable-concurrent-hashmap [] (ConcurrentHashMap.)) :cljs (defn make-mutable-concurrent-hashmap [] (atom {}))) #?(:clj (defn mch->immutable-map [^ConcurrentHashMap mh] (into {} mh)) :cljs (defn mch->immutable-map [mh] @mh)) #?(:clj (defn mch-put [^ConcurrentHashMap mh k v] (.put mh k v)) :cljs (defn mch-put [mh k v] (swap! mh assoc k v))) #?(:clj (defn mch-contains? [^ConcurrentHashMap mh k] (.containsKey mh k)) :cljs (defn mch-contains? [mh k] (contains? @mh k))) #?(:clj (defn mch-keys [^ConcurrentHashMap mh] (enumeration-seq (.keys mh))) :cljs (defn mch-keys [mh] (keys @mh))) #?(:clj (defn mch-get [^ConcurrentHashMap mh k] (.get mh k)) :cljs (defn mch-get [mh k] (get @mh k))) #?(:clj (defn mch-remove [^ConcurrentHashMap mh k] (.remove mh k)) :cljs (defn mch-remove [mh k] (swap! mh dissoc k))) ;;;;;;;;;;;;; ;; Int Map ;; ;;;;;;;;;;;;; #?(:clj (defn int-map [] (int-map/int-map)) :cljs (defn int-map [] {})) ================================================ FILE: src-inst/flow_storm/runtime/outputs.cljc ================================================ (ns flow-storm.runtime.outputs (:require [flow-storm.runtime.events :as rt-events] [flow-storm.runtime.values :as rt-values] [amalloy.ring-buffer :refer [ring-buffer]])) (defonce *last-evals-results (atom (ring-buffer 10))) (defonce *tap-fn (atom nil)) (defn setup-tap! [] (when-not @*tap-fn (let [tap-f (fn [v] (let [vref (rt-values/reference-value! v)] (rt-events/publish-event! (rt-events/make-tap-event vref))))] (add-tap tap-f) (reset! *tap-fn tap-f)))) (defn remove-tap! [] (when-let [tfn @*tap-fn] (remove-tap tfn) (reset! *tap-fn nil))) (defn- fire-update-last-evals-event [] (let [last-evals-refs (->> @*last-evals-results (mapv rt-values/reference-value!))] (rt-events/publish-event! (rt-events/make-last-evals-update-event last-evals-refs)))) (defn clear-outputs [] (reset! *last-evals-results (ring-buffer 10)) (fire-update-last-evals-event)) (defn handle-eval-result [v] (swap! *last-evals-results conj v) (fire-update-last-evals-event)) (defn handle-out-write [s] (rt-events/publish-event! (rt-events/make-out-write-event s))) (defn handle-err-write [s] (rt-events/publish-event! (rt-events/make-err-write-event s))) ================================================ FILE: src-inst/flow_storm/runtime/types/bind_trace.cljc ================================================ (ns flow-storm.runtime.types.bind-trace (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.utils :as utils])) (def nil-idx -1) (defn- print-it [bind] (utils/format "#flow-storm/bind-trace [Symbol: %s, ValType: %s, Coord: %s]" (index-protos/get-bind-sym-name bind) (pr-str (type (index-protos/get-bind-val bind))) (index-protos/get-coord-raw bind))) (deftype BindTrace [symName val coord ^int visibleAfterIdx] index-protos/CoordableTimelineEntryP (get-coord-vec [_] (utils/str-coord->vec coord)) (get-coord-raw [_] coord) index-protos/BindTraceP (get-bind-sym-name [_] symName) (get-bind-val [_] val) index-protos/ImmutableP (as-immutable [this] {:type :bind :symbol (index-protos/get-bind-sym-name this) :value (index-protos/get-bind-val this) :coord (index-protos/get-coord-vec this) :visible-after visibleAfterIdx}) #?@(:cljs [IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-it this)))])) #?(:clj (defmethod print-method BindTrace [bind #?@(:clj [^java.io.Writer w] :cljs [w])] (.write w ^String (print-it bind)))) (defn make-bind-trace [sym-name val coord visible-after-idx] (->BindTrace sym-name val coord visible-after-idx)) (defn bind-trace? [x] (and x (instance? BindTrace x))) ================================================ FILE: src-inst/flow_storm/runtime/types/expr_trace.cljc ================================================ (ns flow-storm.runtime.types.expr-trace (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.utils :as utils])) (def nil-idx -1) (defn- print-it [expr] (utils/format "#flow-storm/expr-trace [Coord: %s, Type: %s]" (index-protos/get-coord-raw expr) (pr-str (type (index-protos/get-expr-val expr))))) (deftype ExprTrace [coord exprVal ^int fnCallIdx] index-protos/ExpressionTimelineEntryP (get-expr-val [_] exprVal) index-protos/CoordableTimelineEntryP (get-coord-vec [_] (utils/str-coord->vec coord)) (get-coord-raw [_] coord) index-protos/TimelineEntryP (entry-type [_] :expr) index-protos/FnChildTimelineEntryP (fn-call-idx [_] (when (not= fnCallIdx nil-idx) fnCallIdx)) index-protos/ImmutableP (as-immutable [this] {:type :expr :coord (index-protos/get-coord-vec this) :result (index-protos/get-expr-val this) :fn-call-idx (index-protos/fn-call-idx this)}) #?@(:cljs [IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-it this)))])) #?(:clj (defmethod print-method ExprTrace [expr ^java.io.Writer w] (.write w ^String (print-it expr)))) (defn make-expr-trace [coord expr-val fn-call-idx] (->ExprTrace coord expr-val fn-call-idx)) (defn expr-trace? [x] (and x (instance? ExprTrace x))) ================================================ FILE: src-inst/flow_storm/runtime/types/fn_call_trace.cljc ================================================ (ns flow-storm.runtime.types.fn-call-trace (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.runtime.indexes.utils :as index-utils] [flow-storm.utils :as utils])) (def nil-idx -1) (defn- print-it [fn-call] (utils/format "#flow-storm/fn-call-trace [%s/%s]" (index-protos/get-fn-ns fn-call) (index-protos/get-fn-name fn-call))) (deftype FnCallTrace [ fnName fnNs ^int formId fnArgs frameBindings ^:unsynchronized-mutable ^int parentIdx ^:unsynchronized-mutable ^int retIdx] index-protos/FnCallTraceP (get-fn-name [_] fnName) (get-fn-ns [_] fnNs) (get-form-id [_] formId) (get-fn-args [_] fnArgs) (get-ret-idx [_] (when (not= retIdx nil-idx) retIdx)) (set-ret-idx [_ idx] (set! retIdx (int idx))) (get-parent-idx [_] (when (not= parentIdx nil-idx) parentIdx)) (set-parent-idx [_ idx] (set! parentIdx (int idx))) (add-binding [_ bind] (index-utils/ml-add frameBindings bind)) (bindings [_] (into [] frameBindings)) index-protos/TimelineEntryP (entry-type [_] :fn-call) index-protos/ImmutableP (as-immutable [this] {:type :fn-call :fn-name fnName :fn-ns fnNs :form-id formId :fn-args fnArgs :parent-idx (index-protos/get-parent-idx this) :ret-idx (index-protos/get-ret-idx this)}) #?@(:cljs [IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-it this)))])) #?(:clj (defmethod print-method FnCallTrace [fn-call ^java.io.Writer w] (.write w ^String (print-it fn-call)))) (defn make-fn-call-trace [fn-ns fn-name form-id fn-args parent-idx] (->FnCallTrace fn-name fn-ns form-id fn-args (index-utils/make-mutable-list) (or parent-idx nil-idx) nil-idx)) (defn fn-call-trace? [x] (and x (instance? FnCallTrace x))) ================================================ FILE: src-inst/flow_storm/runtime/types/fn_return_trace.cljc ================================================ (ns flow-storm.runtime.types.fn-return-trace (:require [flow-storm.runtime.indexes.protocols :as index-protos] [flow-storm.utils :as utils])) (def nil-idx -1) (defn- print-ret [ret-trace] (utils/format "#flow-storm/return-trace [Coord: %s, Type: %s]" (index-protos/get-coord-raw ret-trace) (pr-str (type (index-protos/get-expr-val ret-trace))))) (deftype FnReturnTrace [ coord retVal ^int fnCallIdx] index-protos/ExpressionTimelineEntryP (get-expr-val [_] retVal) index-protos/CoordableTimelineEntryP (get-coord-vec [_] (utils/str-coord->vec coord)) (get-coord-raw [_] coord) index-protos/TimelineEntryP (entry-type [_] :fn-return) index-protos/FnChildTimelineEntryP (fn-call-idx [_] (when (not= fnCallIdx nil-idx) fnCallIdx)) index-protos/ImmutableP (as-immutable [this] {:type :fn-return :coord (index-protos/get-coord-vec this) :result (index-protos/get-expr-val this) :fn-call-idx (index-protos/fn-call-idx this)}) #?@(:cljs [IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-ret this)))])) #?(:clj (defmethod print-method FnReturnTrace [ret-trace ^java.io.Writer w] (.write w ^String (print-ret ret-trace)))) (defn make-fn-return-trace [coord ret-val fn-call-idx] (->FnReturnTrace coord ret-val fn-call-idx)) (defn fn-return-trace? [x] (and x (instance? FnReturnTrace x))) (defn- print-unw [unwind-trace] (utils/format "#flow-storm/unwind-trace [Coord: %s, ExType: %s]" (index-protos/get-coord-raw unwind-trace) (pr-str (type (index-protos/get-throwable unwind-trace))))) (deftype FnUnwindTrace [ coord throwable ^int fnCallIdx] index-protos/CoordableTimelineEntryP (get-coord-vec [_] (utils/str-coord->vec coord)) (get-coord-raw [_] coord) index-protos/UnwindTimelineEntryP (get-throwable [_] throwable) index-protos/TimelineEntryP (entry-type [_] :fn-unwind) index-protos/FnChildTimelineEntryP (fn-call-idx [_] (when (not= fnCallIdx nil-idx) fnCallIdx)) index-protos/ImmutableP (as-immutable [this] {:type :fn-unwind :coord (index-protos/get-coord-vec this) :throwable (index-protos/get-throwable this) :fn-call-idx (index-protos/fn-call-idx this)}) #?@(:cljs [IPrintWithWriter (-pr-writer [this writer _] (write-all writer (print-unw this)))])) #?(:clj (defmethod print-method FnUnwindTrace [unwind-trace ^java.io.Writer w] (.write w ^String (print-unw unwind-trace)))) (defn make-fn-unwind-trace [coord throwable fn-call-idx] (->FnUnwindTrace coord throwable fn-call-idx)) (defn fn-unwind-trace? [x] (and x (instance? FnUnwindTrace x))) (defn fn-end-trace? [x] (or (fn-return-trace? x) (fn-unwind-trace? x))) ================================================ FILE: src-inst/flow_storm/runtime/values.cljc ================================================ (ns flow-storm.runtime.values (:require [clojure.pprint :as pp] [flow-storm.utils :as utils] [flow-storm.types :as types] [flow-storm.eql :as eql] [clojure.datafy :refer [datafy nav]] [clojure.string :as str])) (defprotocol PWrapped (unwrap [_])) (deftype HashableObjWrap [obj] #?@(:clj [clojure.lang.IHashEq (hasheq [_] (utils/object-id obj)) (hashCode [_] (utils/object-id obj)) (equals [_ that] (if (instance? HashableObjWrap that) (identical? obj (unwrap that)) false))] :cljs [IEquiv (-equiv [_ that] (if (instance? HashableObjWrap that) (identical? obj (unwrap that)) false)) IHash (-hash [_] (utils/object-id obj))]) PWrapped (unwrap [_] obj)) (defn hashable-obj-wrap [o] (->HashableObjWrap o)) (defn hashable-obj-wrapped? [o] (instance? HashableObjWrap o)) (defprotocol PValueRefRegistry (add-val-ref [_ v]) (get-value [_ vref]) (get-value-ref [_ v]) (all-val-ref-tuples [_])) ;; Fast way of going from ;; value-ref -> value ;; value -> value-ref ;; ;; every object gets wrapped into a HashableObjWrap that will have ;; hashCode based on unique object-id, calculated by `utils/object-id` ;; This is so ^:a [] and ^:b [] get a different value-ref, which won't get unless ;; we wrap them, since both will have the same hashCode because meta isn't used for hashCode calculation ;; Wrapping is also useful for infinite sequences, since you can't put a val as a key of a hash-map if it ;; is a infinite seq (defrecord ValueRefRegistry [vref->wv wv->vref max-vid] PValueRefRegistry (add-val-ref [this v] (let [wv (hashable-obj-wrap v)] (if (contains? wv->vref wv) this (let [next-vid (inc max-vid) vref (types/make-value-ref max-vid)] (-> this (assoc :max-vid next-vid) (update :vref->wv assoc vref wv) (update :wv->vref assoc wv vref)))))) (get-value [_ vref] (unwrap (get vref->wv vref))) (get-value-ref [_ v] (let [wv (hashable-obj-wrap v)] (get wv->vref wv))) (all-val-ref-tuples [_] (->> (seq wv->vref) (map (fn [[wv vref]] [(unwrap wv) vref]))))) (defn make-empty-value-ref-registry [] (map->ValueRefRegistry {:vref->wv {} :wv->vref {} :max-vid 0})) (defonce values-ref-registry (atom (make-empty-value-ref-registry))) (defn deref-value [vref] (if (types/value-ref? vref) (get-value @values-ref-registry vref) ;; if vref is not a ref, assume it is a value and just return it vref)) (defn deref-val-id [vid] (deref-value (types/make-value-ref vid))) (defn reference-value! [v] (try (swap! values-ref-registry add-val-ref v) (-> (get-value-ref @values-ref-registry v) (types/add-val-preview v)) ;; if for whatever reason we can't reference the value ;; let's be explicit so at least the user knows that ;; something went wrong and the value can't be trusted. ;; I have seen a issues of hashing failing for a lazy sequence #?(:clj (catch Exception e (utils/log-error "Error referencing value" e) (reference-value! :flow-storm/error-referencing-value)) :cljs (catch js/Error e (utils/log-error "Error referencing value" e) (reference-value! :flow-storm/error-referencing-value))))) (defn clear-vals-ref-registry [] (reset! values-ref-registry (make-empty-value-ref-registry))) (defprotocol SnapshotP (snapshot-value [_])) (extend-protocol SnapshotP #?(:clj Object :cljs default) (snapshot-value [v] v)) (extend-protocol SnapshotP nil (snapshot-value [_] nil)) (extend-protocol SnapshotP #?(:clj clojure.lang.Atom :cljs cljs.core/Atom) (snapshot-value [a] {:ref/snapshot (deref a) :ref/type (type a)})) #?(:clj (extend-protocol SnapshotP clojure.lang.Agent (snapshot-value [a] {:ref/snapshot (deref a) :ref/type (type a)}))) #?(:clj (extend-protocol SnapshotP clojure.lang.Ref (snapshot-value [r] {:ref/snapshot (deref r) :ref/type (type r)}))) #?(:clj (extend-protocol SnapshotP clojure.lang.Var (snapshot-value [v] {:ref/snapshot (deref v) :ref/type (type v)}))) (defn snapshot-reference [x] (if (and (utils/derefable? x) (utils/pending? x) (realized? x)) ;; If the value is already realized it should be safe to call deref. ;; If it is a non realized pending derefable we don't mess with it ;; because we don't want to interfere with the program behavior, since ;; people can do side effectful things on deref, like in the case of delay ;; This will cover basically realized promises, futures and delays {:ref/snapshot (deref x) :ref/type (type x)} (snapshot-value x))) (defn value-type [v] (if (and (map? v) (try ;; the try/catch is for things like sorted-map of symbol keys (contains? v :ref/type) (catch #?(:clj Exception :cljs js/Error) _e nil))) (pr-str (:ref/type v)) (pr-str (type v)))) (defn val-pprint [val {:keys [print-length print-level print-meta? pprint? nth-elems]}] (let [val-type (value-type val) print-fn #?(:clj (if pprint? pp/pprint prn) :cljs (if (and pprint? (not print-meta?)) pp/pprint print)) ;; ClojureScript pprint doesn't support *print-meta* val-str (try (if (and (utils/blocking-derefable? val) (utils/pending? val)) "FlowStorm : Unrealized value" (binding [*print-level* print-level *print-length* print-length *print-meta* print-meta?] (if nth-elems (let [max-idx (dec (count val)) nth-valid-elems (filter #(<= % max-idx) nth-elems) printed-elems (->> nth-valid-elems (mapv (fn [n] (str/trim-newline (with-out-str (print-fn (nth val n)))))))] (str "[" (str/join " " printed-elems) "]")) (with-out-str (print-fn val))))) ;; return somthing so the user knows the value can't be trusted #?(:clj (catch Exception e (utils/log-error "Error pprinting value" e) "Flow-storm error, value couldn't be pprinted") :cljs (catch js/Error e (utils/log-error "Error pprinting value" e) "Flow-storm error, value couldn't be pprinted")))] {:val-str val-str :val-type val-type})) (defn val-pprint-ref [vref opts] (let [val (deref-value vref)] (val-pprint val opts))) (defn tap-value [vref] (let [v (deref-value vref)] (tap> v))) #?(:clj (defn def-value [var-ns var-name x] (intern (symbol var-ns) (symbol var-name) (deref-value x)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defonce values-datafiers-registry (atom {})) (defn register-data-aspect-extractor [{:keys [id] :as extractor}] (swap! values-datafiers-registry assoc id extractor)) (defn unregister-data-aspect-extractor [id] (swap! values-datafiers-registry dissoc id)) (defn interesting-nav-references [coll ks map-coll?] (reduce (fn [nfs k] (let [v (get coll k) n (nav coll k v)] (if (or (not= n v) (not= (meta n) (meta v))) (assoc nfs (if map-coll? (reference-value! k) k) (reference-value! n)) nfs))) {} ks)) (defn extract-data-aspects [o extra] (let [dat-o (datafy o) o-meta (meta o)] (reduce (fn [aspects {:keys [id pred extractor]}] (if (try (pred dat-o extra) ;; To make breaking change smooth, remove after some time #?(:clj (catch clojure.lang.ArityException _ (utils/log (utils/format "WARNING! Your aspect extractor predicate with id %s has a one arg extractor function which is deprecated. Please upgrade it to (fn [v extra] ...)" id)) (pred dat-o)) :cljs (catch js/Error _ (utils/log (utils/format "WARNING! Your aspect extractor predicate with id %s has a one arg extractor function which is deprecated. Please upgrade it to (fn [v extra] ...)" id)) (pred dat-o)) )) (let [ext (try (extractor dat-o extra) ;; To make breaking change smooth, remove after some time #?(:clj (catch Throwable e (utils/log-error (utils/format "Problem running %s on object of type %s" id (type dat-o)) e) {}) :cljs (catch js/Error e (utils/log-error (utils/format "Problem running %s on object of type %s" id (type dat-o)) e) {}) ))] (-> ext (merge aspects) (update ::kinds conj id))) aspects)) (cond-> {::kinds #{} ::type (pr-str (type o)) ::val-ref (reference-value! o)} o-meta (assoc ::meta-ref (reference-value! o-meta))) (vals @values-datafiers-registry)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data aspect extractors ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;; (register-data-aspect-extractor {:id :number :pred (fn [x _] (number? x)) :extractor (fn [n _] {:number/val n})}) (register-data-aspect-extractor {:id :string :pred (fn [x _] (string? x)) :extractor (fn [s _] {:string/val s})}) (register-data-aspect-extractor {:id :int :pred (fn [x _] (int? x)) :extractor (fn [n _] {:int/decimal n :int/binary (utils/format-int n 2) :int/octal (utils/format-int n 8) :int/hex (utils/format-int n 16)})}) (register-data-aspect-extractor {:id :previewable :pred (fn [x _] (any? x)) :extractor (fn [o {:keys [pprint-previews?]}] {:preview/pprint (-> o (val-pprint {:pprint? pprint-previews? :print-length 10 :print-level 5 :print-meta? false}) :val-str)})}) (register-data-aspect-extractor {:id :shallow-map :pred (fn [x _] (map? x)) :extractor (fn [m _] (let [m-keys (keys m) m-vals (vals m)] {:shallow-map/keys-refs (mapv reference-value! m-keys) :shallow-map/navs-refs (interesting-nav-references m m-keys true) :shallow-map/vals-refs (mapv reference-value! m-vals)}))}) (register-data-aspect-extractor {:id :paged-shallow-seqable :pred (fn [x _] (seqable? x)) :extractor (fn [s _] (let [xs (seq s) page-size 100 last-page? (< (bounded-count page-size xs) page-size) page (take page-size xs)] (cond-> {:paged-seq/count (when (counted? s) (count s)) :paged-seq/page-size page-size :paged-seq/page-refs (mapv reference-value! page)} (not last-page?) (assoc :paged-seq/next-ref (reference-value! (drop page-size xs))))))}) (register-data-aspect-extractor {:id :shallow-indexed :pred (fn [x _] (and (indexed? x) (not (utils/transient? x)))) :extractor (fn [idx-coll _] {:shallow-idx-coll/count (count idx-coll) :shallow-idx-coll/vals-refs (reduce (fn [acc idx] (conj acc (reference-value! (nth idx-coll idx)))) [] (range (count idx-coll))) :shallow-idx-coll/navs-refs (try (interesting-nav-references idx-coll (range (count idx-coll)) false) #?(:clj (catch Exception e (utils/log-error (utils/format "Warning, couldn't get navigation references for indexed collection of type %s" (type idx-coll)) e) nil) :cljs (catch js/Error e (utils/log-error (utils/format "Warning, couldn't get navigation references for indexed collection of type %s" (type idx-coll)) e) nil)))})}) #?(:clj (register-data-aspect-extractor {:id :byte-array :pred (fn [x _] (bytes? x)) :extractor (fn [bs _] (let [max-cnt 1000 format-and-pad (fn [b radix] (let [s (-> ^byte b Byte/toUnsignedInt (utils/format-int radix)) s-padded (cond (= radix 2) (format "%8s" s) (= radix 16) (format "%2s" s))] (str/replace s-padded " " "0")))] (if (<= (count bs) max-cnt) {:bytes/hex (mapv #(format-and-pad % 16) bs) :bytes/binary (mapv #(format-and-pad % 2) bs) :bytes/full? true} (let [head (take (/ max-cnt 2) bs) tail (drop (- (count bs) (/ max-cnt 2)) bs)] {:bytes/head-hex (mapv #(format-and-pad % 16) head) :bytes/head-binary (mapv #(format-and-pad % 2) head) :bytes/tail-hex (mapv #(format-and-pad % 16) tail) :bytes/tail-binary (mapv #(format-and-pad % 2) tail)}))))})) (defprotocol ScopeFrameSampleP (sample-chan-1 [_]) (sample-chan-2 [_])) (defprotocol ScopeFrameP :extend-via-metadata true (frame-samp-rate [_]) (frame-samples [_])) (register-data-aspect-extractor {:id :oscilloscope-samples-frames :pred (fn [x _] (or (satisfies? ScopeFrameP x) (-> (get (meta x) `frame-samples)))) :extractor (fn [frame _] {:frame frame})}) (defn register-eql-query-pprint-extractor [] ;; Let's leave this disable by default for now since it has some ;; performance implications. (register-data-aspect-extractor {:id :eql-query-pprint :pred (fn [x _] (coll? x)) :extractor (fn [coll {:keys [query pprint-previews?]}] (let [query (or query '[*]) q-pprint (-> (eql/eql-query coll query) (val-pprint {:pprint? pprint-previews? :print-meta? false :print-length 1000}) :val-str)] {:eql/pprint q-pprint :eql/query query}))})) (comment (extract-data-aspects 120 nil) (extract-data-aspects {:a 20 :b 40} nil) (extract-data-aspects [1 2 3] nil) (extract-data-aspects (range 100) nil) ) ================================================ FILE: src-inst/flow_storm/storm_api.clj ================================================ (ns flow-storm.storm-api (:require [flow-storm.api :as fs-api] [flow-storm.tracer :as tracer] [flow-storm.runtime.debuggers-api :as dbg-api])) (defn start-recorder "Called by ClojureStorm for initializing the runtime" [] (dbg-api/start-runtime)) (defn start-debugger "Called by :dbg keyword evaluation to start the UI" [] (fs-api/local-connect {})) (def jump-to-last-expression dbg-api/jump-to-last-expression-in-this-thread) (defn print-flow-storm-help [] (println "Flow Storm settings: \n") (println (format " Recording : %s" (tracer/recording?))) (println) (println "ClojureStorm Commands: \n") (println " :dbg - Show the FlowStorm debugger UI, you can dispose it by closing the window.") (println " :rec - Start recording. All instrumented code traces will be recorded.") (println " :stop - Stop recording. Instrumented code will execute but nothing will be recorded, so no extra heap will be consumed.") (println " :help - Print this help.") (println) (println "JVM config properties: \n") (println " -Dflowstorm.startRecording [true|false]") (println " -Dflowstorm.theme [dark|light|auto] (defaults to auto)") (println " -Dflowstorm.styles [STRING] Ex: /home/user/my-styles.css") (println " -Dflowstorm.threadFnCallLimits Ex: org.my-app/fn1:2,org.my-app/fn2:4") (println " -Dclojure.storm.instrumentOnlyPrefixes Ex: my-app,my-lib") (println " -Dclojure.storm.instrumentSkipPrefixes Ex: my-app.too-heavy,my-lib.uninteresting") (println " -Dclojure.storm.instrumentSkipRegex Ex: .*test.*") (println) (println "Modify limits : \n") (println " (flow-storm.runtime.indexes.api/add-fn-call-limit \"org.my-app\" \"fn1\" 10)") (println " (flow-storm.runtime.indexes.api/rm-fn-call-limit \"org.my-app\" \"fn1\")") (println " (flow-storm.runtime.indexes.api/get-fn-call-limits)") (println) (println)) (defn maybe-execute-flow-storm-specials [input] (try (case input :dbg (do (start-debugger) true) :last (do (jump-to-last-expression) true) :rec (do (dbg-api/set-recording true) true) :stop (do (dbg-api/set-recording false) true) :tut/basics (println "Deprecated. Start the UI by evaluating the :dbg keyword and find the tutorial under the Help menu.") false) (catch Exception e (if (or (instance? UnsupportedClassVersionError e) (and (.getCause e) (instance? UnsupportedClassVersionError (.getCause e)))) (do (println "\n\nFlowStorm UI requires JDK >= 17. If you can't upgrade your JDK you can still use it by downgrading JavaFx.") (println "\nIf that is the case add this dependencies to your alias :\n") (println " org.openjfx/javafx-controls {:mvn/version \"19.0.2\"}") (println " org.openjfx/javafx-base {:mvn/version \"19.0.2\"}") (println " org.openjfx/javafx-graphics {:mvn/version \"19.0.2\"}") (println " org.openjfx/javafx-web {:mvn/version \"19.0.2\"}") (println)) ;; else (.printStackTrace e)) false))) ================================================ FILE: src-inst/flow_storm/storm_preload.cljs ================================================ (ns flow-storm.storm-preload (:require [cljs.storm.tracer] [flow-storm.tracer :as tracer] [flow-storm.runtime.debuggers-api :as dbg-api])) (def dbg-port (js/parseInt (if js/window (let [page-params (-> js/window .-location .-search) url-params (js/URLSearchParams. page-params)] (or (.get url-params "flowstorm_ws_port") "7722")) ;; for node js "7722"))) (dbg-api/start-runtime) (tracer/hook-clojurescript-storm) (dbg-api/remote-connect {:debugger-host "localhost" :debugger-ws-port dbg-port}) ================================================ FILE: src-inst/flow_storm/tracer.cljc ================================================ (ns flow-storm.tracer (:require [flow-storm.utils :as utils :refer [stringify-coord]] [flow-storm.runtime.values :refer [snapshot-reference]] [flow-storm.runtime.indexes.api :as indexes-api] [flow-storm.runtime.events :as rt-events])) (declare start-tracer) (declare stop-tracer) (defonce total-order-recording (atom false)) (defonce recording (atom false)) (defonce breakpoints (atom #{})) (defonce blocked-threads (atom #{})) (defonce current-flow-id (atom 0)) (defonce thread-trace-limit 0) (defonce throw-on-trace-limit? false) (defonce heap-limit nil) (defn set-recording [enable?] (if enable? (do (reset! recording true) (indexes-api/reset-all-threads-trees-build-stack @current-flow-id)) (reset! recording false)) (rt-events/publish-event! (rt-events/make-recording-updated-event enable?))) (defn set-multi-timeline-recording [enable?] (reset! total-order-recording (boolean enable?)) (rt-events/publish-event! (rt-events/make-multi-timeline-recording-updated-event enable?))) (defn stop-recording [] (set-recording false) (set-multi-timeline-recording false)) (defn recording? [] @recording) (defn multi-timeline-recording? [] @total-order-recording) (defn set-current-flow-id [flow-id] (reset! current-flow-id flow-id)) (defn get-current-flow-id [] @current-flow-id) (defn set-thread-trace-limit [{:keys [limit break?]}] #?(:clj (do (alter-var-root #'thread-trace-limit (constantly limit)) (alter-var-root #'throw-on-trace-limit? (constantly break?))) :cljs (do (set! thread-trace-limit limit) (set! throw-on-trace-limit? break?)))) (defn set-heap-limit [{:keys [limit break?]}] #?(:clj (do (alter-var-root #'heap-limit (constantly limit)) (alter-var-root #'throw-on-trace-limit? (constantly break?))) :cljs (do (set! heap-limit limit) (set! throw-on-trace-limit? break?)))) #?(:clj (defn- block-this-thread [flow-id breakpoint] (let [thread-obj (Thread/currentThread) tname (utils/get-current-thread-name) tid (utils/get-current-thread-id)] (if (= tname"JavaFX Application Thread") (utils/log "WARNING, skipping thread block, trace is being executed by the UI thread and doing so will freeze the UI.") (do (utils/log (format "Blocking thread %d %s" tid tname)) (indexes-api/mark-thread-blocked flow-id tid breakpoint) (swap! blocked-threads conj thread-obj) (locking thread-obj (.wait thread-obj)) (indexes-api/mark-thread-unblocked flow-id tid) (utils/log (format "Thread %d %s unlocked, continuing ..." tid tname))))))) #?(:clj (defn unblock-thread [thread-id] (let [thread-obj (utils/get-thread-object-by-id thread-id)] (swap! blocked-threads disj thread-obj) (locking thread-obj (.notifyAll thread-obj))))) #?(:clj (defn unblock-all-threads [] (doseq [thread-obj @blocked-threads] (locking thread-obj (.notifyAll thread-obj))))) (defn add-breakpoint! [fn-ns fn-name args-pred] (swap! breakpoints conj (with-meta [fn-ns fn-name] {:predicate args-pred}))) (defn remove-breakpoint! [fn-ns fn-name] (swap! breakpoints disj [fn-ns fn-name])) (defn clear-breakpoints! [] (reset! breakpoints #{})) (defn all-breakpoints [] @breakpoints) (defn trace-form-init "Send form initialization trace only once for each thread." [{:keys [form-id ns def-kind dispatch-val form file line]}] (when-not (indexes-api/get-form form-id) (let [trace (cond-> {:trace/type :form-init :form/id form-id :form/form form :form/ns ns :form/def-kind def-kind :form/file file :form/line line} (= def-kind :defmethod) (assoc :multimethod/dispatch-val dispatch-val))] (indexes-api/add-form-init-trace trace)))) (defn- check-limits [flow-id thread-id] (let [limit-hit? (if (and heap-limit (> (utils/get-used-memory-mb) heap-limit)) (if throw-on-trace-limit? (throw (ex-info "Heap limit reached" {})) (do (stop-recording) true)) (if (and (pos? thread-trace-limit) (> (count (indexes-api/get-timeline flow-id thread-id)) thread-trace-limit)) (if throw-on-trace-limit? (throw (ex-info "Thread limit reached" {})) true) false))] limit-hit?)) (defn trace-fn-call "Send function call traces" ([{:keys [form-id ns fn-name fn-args]}] ;; for using with hansel (trace-fn-call nil ns fn-name fn-args form-id)) ([_ fn-ns fn-name fn-args form-id] ;; for using with storm (let [flow-id @current-flow-id thread-id (utils/get-current-thread-id) thread-name (utils/get-current-thread-name)] (when @recording #?(:clj (let [brks @breakpoints] (when (and (pos? (count brks)) (contains? brks [fn-ns fn-name]) (apply (-> (get brks [fn-ns fn-name]) meta :predicate) fn-args)) (block-this-thread flow-id [fn-ns fn-name])))) (let [limit-hit? (check-limits flow-id thread-id)] (when-not limit-hit? (let [args (mapv snapshot-reference fn-args)] (indexes-api/add-fn-call-trace flow-id thread-id thread-name fn-ns fn-name form-id args @total-order-recording)))))))) (defn trace-fn-return "Send function return traces" ([{:keys [return coor form-id]}] ;; for using with hansel (trace-fn-return nil return (stringify-coord coor) form-id) return) ([_ return coord _] ;; for using with storm (when @recording (let [flow-id @current-flow-id thread-id (utils/get-current-thread-id) limit-hit? (check-limits flow-id thread-id)] (when-not limit-hit? (indexes-api/add-fn-return-trace flow-id thread-id coord (snapshot-reference return) @total-order-recording)))))) (defn trace-fn-unwind ([{:keys [throwable coor form-id]}] ;; for using with hansel (trace-fn-unwind nil throwable (stringify-coord coor) form-id)) ([_ throwable coord _] ;; for using with storm (when @recording (let [flow-id @current-flow-id thread-id (utils/get-current-thread-id) limit-hit? (check-limits flow-id thread-id)] (when-not limit-hit? (indexes-api/add-fn-unwind-trace flow-id thread-id coord (snapshot-reference throwable) @total-order-recording)))))) (defn trace-expr-exec "Send expression execution trace." ([{:keys [result coor form-id]}] ;; for using with hansel (trace-expr-exec nil result (stringify-coord coor) form-id) result) ([_ result coord _] ;; for using with storm (when @recording (let [flow-id @current-flow-id thread-id (utils/get-current-thread-id) limit-hit? (check-limits flow-id thread-id)] (when-not limit-hit? (indexes-api/add-expr-exec-trace flow-id thread-id coord (snapshot-reference result) @total-order-recording)))))) (defn trace-bind "Send bind trace." ([{:keys [symb val coor]}] ;; for using with hansel (trace-bind nil (stringify-coord coor) (name symb) val)) ([_ coord sym-name val] ;; for using with storm (when @recording (let [flow-id @current-flow-id thread-id (utils/get-current-thread-id) limit-hit? (check-limits flow-id thread-id)] (when-not limit-hit? (indexes-api/add-bind-trace flow-id thread-id coord sym-name (snapshot-reference val))))))) (defn hansel-config "Builds a hansel config from inst-opts" [{:keys [disable] :or {disable #{}}}] (cond-> `{:trace-form-init trace-form-init :trace-fn-call trace-fn-call :trace-fn-return trace-fn-return :trace-fn-unwind trace-fn-unwind :trace-expr-exec trace-expr-exec :trace-bind trace-bind} (disable :expr-exec) (dissoc :trace-expr-exec) (disable :bind) (dissoc :trace-expr-exec) (disable :anonymous-fn) (assoc :disable #{:anonymous-fn}))) #?(:clj (defn- set-clojure-storm [callbacks] ;; Set ClojureStorm callbacks by reflection so FlowStorm can be used ;; without ClojureStorm on the classpath. (let [tracer-class (Class/forName "clojure.storm.Tracer") setTraceFnsCallbacks (.getMethod tracer-class "setTraceFnsCallbacks" (into-array java.lang.Class [clojure.lang.IPersistentMap]))] (.invoke setTraceFnsCallbacks nil (into-array [callbacks]))))) #?(:clj (defn hook-clojure-storm [] (set-clojure-storm {:trace-fn-call-fn trace-fn-call :trace-fn-return-fn trace-fn-return :trace-fn-unwind-fn trace-fn-unwind :trace-expr-fn trace-expr-exec :trace-bind-fn trace-bind}))) #?(:clj (defn unhook-clojure-storm [] (set-clojure-storm {:trace-fn-call-fn nil :trace-fn-return-fn nil :trace-fn-unwind-fn nil :trace-expr-fn nil :trace-bind-fn nil}))) #?(:cljs (defn hook-clojurescript-storm [] ;; We do it like this so FlowStorm can be used without ClojureScript storm, ;; which we can't if we require cljs.storm at the top. (js* "try { cljs.storm.tracer.trace_expr_fn=flow_storm.tracer.trace_expr_exec; cljs.storm.tracer.trace_fn_call_fn=flow_storm.tracer.trace_fn_call; cljs.storm.tracer.trace_fn_return_fn=flow_storm.tracer.trace_fn_return; cljs.storm.tracer.trace_fn_unwind_fn=flow_storm.tracer.trace_fn_unwind; cljs.storm.tracer.trace_bind_fn=flow_storm.tracer.trace_bind; cljs.storm.tracer.trace_form_init_fn=flow_storm.tracer.trace_form_init; console.log(\"ClojureScriptStorm functions plugged in.\"); } catch (error) {console.log(\"ClojureScriptStorm not detected.\")}") ;; we need to return nil here, the js* can't be the last statement or it will ;; generate "return try {...}" which isn't valid JS nil)) ================================================ FILE: src-shared/flow_storm/eql.cljc ================================================ (ns flow-storm.eql (:require [flow-storm.utils :as utils])) (defn entity? [data] (and (map? data) (every? keyword? (keys data)))) (defn eql-query [data q] (cond (not (coll? data)) data (entity? data) (reduce (fn [res-map sel] (cond (and (symbol? sel) (#{'* '?} sel)) (let [ks (into [] (keys data))] (cond (= '* sel) (eql-query data ks) (= '? sel) ks)) (not (coll? sel)) (assoc res-map sel (get data sel)) (map? sel) (reduce-kv (fn [mq-res-map k q] (if (contains? data k) (assoc mq-res-map k (eql-query (get data k) q)) mq-res-map)) res-map sel) )) {} q) ;; non entity maps (map? data) (utils/update-values data #(eql-query % q)) ;; be carefull with infinite sequences (not (counted? data)) (let [length-limit 1000 proc-data (map #(eql-query % q) data)] (if (< (bounded-count length-limit data) length-limit) proc-data (conj (take length-limit proc-data) :flow-storm/not-counted-truncated))) (list? data) (map #(eql-query % q) data) (vector? data) (mapv #(eql-query % q) data) ;; every other collection :else (into (empty data) (map #(eql-query % q)) data))) (comment (def data [{:name "Bob" :age 41 :houses {1 {:rooms 5 :address "A"} 2 {:rooms 3 :address "B"}}} {:name "Alice" :age 32 :vehicles [{:type :car :wheels 4 :seats #{{:kind :small :position :left} {:kind :small :position :right} {:kind :big :position :center}}} {:type :bike :wheels 2}] :infinite (cycle [{:name "Bob" :age 41} {:name "Alice" :age 32}])}]) (tap> data) (eql-query (range) '[*]) (eql-query data '[*]) (eql-query data '[:name]) (eql-query data '[:name :age :vehicles]) (eql-query data '[:name :age {:vehicles [:type]}]) (eql-query data '[:name :age {:vehicles [?]}]) (eql-query data '[:name {:vehicles [*]}]) (eql-query data '[:name :age {:vehicles [:type {:seats [?]}]}]) (eql-query data '[:name :age {:vehicles [:type {:seats [:kind]}]}]) (eql-query data '[:name {:infinite [:age]}]) (eql-query data '[:name {:houses [:rooms]}]) ) ================================================ FILE: src-shared/flow_storm/form_pprinter.clj ================================================ (ns flow-storm.form-pprinter (:require [clojure.pprint :as pp] [flow-storm.utils :as utils] [hansel.utils :as hansel-utils]) (:import [java.util ArrayDeque])) (defn- seq-delims "Given a seq? map? or set? form returns a vector with the open and closing delimiters." [form] (let [delims (pr-str (empty form))] (if (= (count delims) 2) [(str (first delims)) (str (second delims))] ["#{" "}"]))) (defn- form-tokens "Given a form returns a collection of tokens. If the form contains ::coord meta it will be attached to the tokens. Eg for the form (with-meta '(+ 1 2) {::coord [1]}) it will return : [{:coord [1], :kind :text, :text \"(\"} {:kind :text, :text \"+\"} {:kind :text, :text \"1\"} {:kind :text, :text \"2\"} {:coord [1], :kind :text, :text \")\"}]" [form] (let [curr-coord (::coord (meta form)) curr-line (:line (meta form)) tok (if curr-coord {:coord curr-coord :line curr-line} {})] (cond (or (seq? form) (vector? form) (set? form)) (let [[db de] (seq-delims form)] (-> [(assoc tok :kind :text :text db)] (into (mapcat (fn [f] (form-tokens f)) form)) (into [(assoc tok :kind :text :text de)]))) (map? form) (let [keys-vals (mapcat identity form) keys-vals-tokens (mapcat (fn [f] (form-tokens f)) keys-vals)] (-> [(assoc tok :kind :text :text "{")] (into keys-vals-tokens) (into [(assoc tok :kind :text :text "}")]))) :else [(assoc tok :kind :text :text (pr-str form))]))) (defn- consecutive-layout-tokens "Given a map of {positions -> tokens} and a idx return a vector of all the consecutive tokens by incrementing idx. Will stop at the first gap. " [pos->layout-token idx] (loop [i (inc idx) layout-tokens [(pos->layout-token idx)]] (if-let [ltok (pos->layout-token i)] (recur (inc i) (conj layout-tokens ltok)) layout-tokens))) ;; This is a fix for https://ask.clojure.org/index.php/13455/clojure-pprint-pprint-bug-when-using-the-code-dispatch-table (defn- pprint-let [alis] (let [base-sym (first alis)] (if (and (next alis) (vector? (second alis))) (pp/pprint-logical-block :prefix "(" :suffix ")" (do ((pp/formatter-out "~w ~1I~@_") base-sym) (#'pp/pprint-binding-form (second alis)) ((pp/formatter-out " ~_~{~w~^ ~_~}") (next (rest alis))))) (#'pp/pprint-simple-code-list alis)))) (def hacked-code-table (#'pp/two-forms (#'pp/add-core-ns {'def #'pp/pprint-hold-first, 'defonce #'pp/pprint-hold-first, 'defn #'pp/pprint-defn, 'defn- #'pp/pprint-defn, 'defmacro #'pp/pprint-defn, 'fn #'pp/pprint-defn, 'let #'pprint-let, 'loop #'pprint-let, 'binding #'pprint-let, 'with-local-vars #'pprint-let, 'with-open #'pprint-let, 'when-let #'pprint-let, 'if-let #'pprint-let, 'doseq #'pprint-let, 'dotimes #'pprint-let, 'when-first #'pprint-let, 'if #'pp/pprint-if, 'if-not #'pp/pprint-if, 'when #'pp/pprint-if, 'when-not #'pp/pprint-if, 'cond #'pp/pprint-cond, 'condp #'pp/pprint-condp, 'fn* #'pp/pprint-simple-code-list, ;; <--- all for changing this from `pp/pprint-anon-func` to `pp/pprint-simple-code-list` ;; so it doesn't substitute anonymous functions '. #'pp/pprint-hold-first, '.. #'pp/pprint-hold-first, '-> #'pp/pprint-hold-first, 'locking #'pp/pprint-hold-first, 'struct #'pp/pprint-hold-first, 'struct-map #'pp/pprint-hold-first, 'ns #'pp/pprint-ns }))) (defn code-pprint [form] ;; Had to hack pprint like this because code pprinting replace (fn [arg#] ... arg# ...) with #(... % ...) ;; and #' with var, deref with @ etc, wich breaks our pprintln system ;; This is super hacky! because I wasn't able to use with-redefs (it didn't work) I replace ;; the pprint method for ISeqs for the duration of our printing (#'pp/use-method pp/code-dispatch clojure.lang.ISeq (fn [alis] ;; <---- this hack disables reader macro sustitution (if-let [special-form (hacked-code-table (first alis))] (special-form alis) (#'pp/pprint-simple-code-list alis)))) (binding [pp/*print-pprint-dispatch* pp/code-dispatch pp/*code-table* hacked-code-table] (let [pprinted-form-str (utils/normalize-newlines (with-out-str (pp/pprint form)))] ;; restore the original pprint so we don't break it (#'pp/use-method pp/code-dispatch clojure.lang.ISeq #'pp/pprint-code-list) pprinted-form-str))) (defn pprint-tokens "Given a form, returns a vector of tokens to pretty print it. Tokens can be any of : - {:kind :text, :text STRING, :idx-from INT, :len INT :coord COORD :line :LINE} - {:kind :sp} - {:kind :nl}" [form] (let [form (hansel-utils/tag-form-recursively form ::coord) pprinted-str (code-pprint form) ;; a map of positions of the form pprinted string that contains spaces or new-lines pos->layout-token (->> pprinted-str (keep-indexed (fn [i c] (cond (= c \newline) [i {:kind :nl}] (= c \space) [i {:kind :sp}] (= c \,) [i {:kind :sp}] :else nil))) (into {})) ;; all the tokens for form, whithout any newline or indentation info pre-tokens (form-tokens form) ;; interleave in pre-tokens newlines and space tokens found by the pprinter final-tokens (loop [[{:keys [text] :as text-tok} & next-tokens] pre-tokens i 0 final-toks []] (if-not text-tok final-toks (if (pos->layout-token i) ;; if there are layout tokens for the current position ;; insert them before the current text-tok (let [consecutive-lay-toks (consecutive-layout-tokens pos->layout-token i)] (recur next-tokens (+ i (count consecutive-lay-toks) (count text)) (-> final-toks (into consecutive-lay-toks) (into [(assoc text-tok :idx-from (+ i (count consecutive-lay-toks)) :len (count text))])))) ;; else just add the text-tok (recur next-tokens (+ i (count text)) (into final-toks [(assoc text-tok :idx-from i :len (count text))])))))] final-tokens)) (defn to-string "Given ptokens as generated by `pprint-tokens` render them into a string." [ptokens] (with-out-str (doseq [{:keys [kind text]} ptokens] (case kind :sp (print " ") :nl (println) :text (print text))))) (defn- read-contiguous-spaces [^ArrayDeque ptokens-q] (loop [len 0] (if (.isEmpty ptokens-q) len (let [{:keys [kind]} (.peek ptokens-q)] (if (= :sp kind) (do (.pop ptokens-q) (recur (inc len))) len))))) (defn- next-span [^ArrayDeque ptokens-q] (when-not (.isEmpty ptokens-q) (let [{:keys [coord kind len interesting? text line]} (.pop ptokens-q)] (cond (= kind :text) {:coord coord, :len len, :interesting? interesting?, :text text, :line line} (= kind :nl) {:len (+ 1 (read-contiguous-spaces ptokens-q)) :tab? true} (= kind :sp) {:len 1})))) (defn coord-spans "Given `ptokens` as generated by `pprint-tokens` return a collection of spans. Spans are maps in the form of {:idx-from INT :len INT} with also some special marks. - If the print-token contains a :coord it will be added to the span - If the print-token is marked as :interesting? so it will be the span - The spans generated for tabs (a new line followed by spaces) will be marked as :tab? true " [ptokens] (let [ptokens-q (ArrayDeque. ptokens)] (loop [idx-from 0 spans []] (if-let [{:keys [len] :as span} (next-span ptokens-q)] (recur (+ idx-from len) (conj spans (assoc span :idx-from idx-from))) spans)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Utilities for the repl ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- debug-print-tokens [ptokens] (-> ptokens to-string println)) (defn pprint-form-hl-coord [form c] (let [tokens (pprint-tokens form)] (doseq [{:keys [kind text coord]} tokens] (let [txt (case kind :sp " " :nl "\n" :text (if (= coord c) (utils/colored-string text :red) text))] (print txt))))) (comment (let [test-form '(defn factorial [n] (if (zero? n) 1 (* n (factorial (dec n)))))] (binding [pp/*print-right-margin* 40 pp/*print-pprint-dispatch* pp/code-dispatch] (= (-> test-form (pprint-tokens) debug-print-tokens with-out-str) (-> test-form pp/pprint with-out-str)))) (form-tokens (with-meta '(+ 1 2) {::coord [1]})) (def test-form '(defn clojurescript-version "Returns clojurescript version as a printable string." [] (fn* [p1__12449#] (+ 1 p1__12449#)) (if (bound? #'*clojurescript-version*) (str (:major *clojurescript-version*) "." (:minor *clojurescript-version*) (when-let [i (:incremental *clojurescript-version*)] (str "." i)) (when-let [q (:qualifier *clojurescript-version*)] (str "." q)) (when (:interim *clojurescript-version*) "-SNAPSHOT")) @synthetic-clojurescript-version))) (binding [pp/*print-right-margin* 80 pp/*print-pprint-dispatch* pp/code-dispatch] (->> test-form (pprint-tokens) #_debug-print-tokens)) ) ================================================ FILE: src-shared/flow_storm/json_serializer.clj ================================================ (ns flow-storm.json-serializer (:require [cognitect.transit :as transit] [flow-storm.utils :refer [log-error]] [flow-storm.types :as types]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream] [java.util.regex Pattern] [flow_storm.types ValueRef])) (def ^ByteArrayOutputStream write-buffer (ByteArrayOutputStream. (* 1024 1024))) ;; 1Mb buffer (defn serialize [obj] (locking write-buffer (try (let [writer (transit/writer write-buffer :json {:handlers {Pattern (transit/write-handler (fn [_] "regex") str) ValueRef (transit/write-handler (fn [_] "flow_storm.types.ValueRef") (fn [vref] (types/serialize-val-ref vref)))} :default-handler (transit/write-handler (fn [_] "object") pr-str)}) _ (transit/write writer obj) ser (.toString write-buffer)] (.reset write-buffer) ser) (catch Exception e (log-error (format "Error serializing %s" obj) e) (throw e))))) (defn deserialize [^String s] (try (let [^ByteArrayInputStream in (ByteArrayInputStream. (.getBytes s)) reader (transit/reader in :json {:handlers {"object" (transit/read-handler (fn [s] s)) "regex" (transit/read-handler (fn [s] (re-pattern s))) "flow_storm.types.ValueRef" (transit/read-handler (fn [vref-str] (-> vref-str read-string types/deserialize-val-ref)))}})] (transit/read reader)) (catch Exception e (log-error (format "Error deserializing %s, ERROR: %s" s (.getMessage e))) (throw e)))) ================================================ FILE: src-shared/flow_storm/json_serializer.cljs ================================================ (ns flow-storm.json-serializer (:require [cognitect.transit :as transit :refer [write-handler]] [cljs.reader :refer [read-string]] [flow-storm.utils :refer [log-error]] [flow-storm.types :as types])) (defn serialize [o] (try (let [writer (transit/writer :json {:handlers {js/RegExp (write-handler (fn [_ _] "regex") str) types/ValueRef (write-handler (fn [_] "flow_storm.types.ValueRef") (fn [vref] (types/serialize-val-ref vref))) :default (write-handler (fn [_ _] "object") pr-str)}})] (transit/write writer o)) (catch js/Error e (log-error (str "Error serializing " o) e) (throw e)))) (defn deserialize [^String s] (try (let [reader (transit/reader :json {:handlers {"object" (fn [s] s) "regex" (fn [s] (re-pattern s)) "flow_storm.types.ValueRef" (fn [vref-str] (-> vref-str read-string types/deserialize-val-ref))}})] (transit/read reader s)) (catch js/Error e (log-error (str "Error deserializing " s " ERROR: " (.-message e))) (throw e)))) ================================================ FILE: src-shared/flow_storm/state_management.cljc ================================================ (ns flow-storm.state-management "Like a smaller simplified version of mount so we don't bring that dependency with us. ") ;; state-name -> {:order :status :start :stop :var} ;; :status [:started | :stopped] (defonce states (atom {})) (defonce last-order (atom 0)) #?(:clj (defn alter-state-var [state-var new-val] (alter-var-root state-var (constantly new-val)))) (defn start-state [{:keys [status start var]} config] (when-not (= status :started) #_(println "Starting " var) (let [new-state (start config)] (alter-state-var var new-state)) (swap! states assoc-in [(str var) :status] :started))) (defn stop-state [{:keys [status stop var]}] (when-not (= status :stopped) #_(println "Stopping " var) (let [new-state (stop)] (alter-state-var var new-state)) (swap! states assoc-in [(str var) :status] :stopped))) (defn current-state [state-name] (when-let [state (get @states state-name)] (deref (:var state)))) (defn register-state [state-name state-map] (let [s (get @states state-name) order (or (:order s) (swap! last-order inc)) new-state-map (assoc state-map :order order)] ;; stop the current state if needed before we lose the var (when s (alter-state-var (:var s) (current-state state-name))) (swap! states assoc state-name new-state-map))) (defn start [{:keys [only config]}] (let [all-states @states effective-states (if only (select-keys all-states (mapv str only)) all-states) ordered-states (->> effective-states vals (sort-by :order <))] (doseq [s ordered-states] (start-state s config)))) (defn stop [{:keys [only]}] (let [all-states @states effective-states (if only (select-keys all-states (mapv str only)) all-states) ordered-states (->> effective-states vals (filter (fn [{:keys [status]}] (= status :started))) (sort-by :order >))] (doseq [s ordered-states] (stop-state s)))) #?(:clj (defmacro defstate [var-name & {:keys [start stop]}] `(do (defonce ~var-name nil) (let [state-name# (str (var ~var-name))] (register-state state-name# {:var (var ~var-name) :start ~start :stop ~stop}))))) (comment ;; (defstate s1 ;; :start (fn [_] (println "Starting s1") :s1) ;; :stop (fn [] (println "Stopping s1"))) ;; (defstate s2 ;; :start (fn [cfg] (println "Starting s2 with config" cfg) :s2) ;; :stop (fn [] (println "Stopping s2"))) ;; (defstate s3 ;; :start (fn [_] (println "Starting s3") :s3) ;; :stop (fn [] (println "Stopping s3"))) ;; (start {:config {:a 10} ;; :only [#'s1 #'s2]}) ;; (stop) ) ================================================ FILE: src-shared/flow_storm/types.cljc ================================================ (ns flow-storm.types (:require [flow-storm.utils :as utils :refer [log-error]])) (defrecord ValueRef [vid]) (defn value-ref? [x] (instance? ValueRef x)) (defn make-value-ref [vid] (->ValueRef vid)) (defn add-val-preview [vref v] (with-meta vref {:val-preview (try (binding [*print-level* 4 *print-length* 20 *print-meta* false] (pr-str v)) #?(:clj (catch Exception e (log-error (utils/format "Couldn't build preview for type %s because of %s" (type v) (.getMessage e)) e) (str (type v))) :cljs (catch js/Error e (log-error (utils/format "Couldn't build preview for type %s because of %s" (type v) (.-message e)) e) (str (type v)))))})) (defn vref-preview [vref] (-> vref meta :val-preview)) (defn serialize-val-ref [vref] (utils/format "[%d %s]" (:vid vref) (-> vref meta :val-preview pr-str))) (defn deserialize-val-ref [[vid vprev]] (with-meta (make-value-ref vid) {:val-preview vprev})) #?(:clj (defmethod print-method ValueRef [vref ^java.io.Writer w] (.write w ^String (str "#flow-storm.types/value-ref " (:vid vref)))) :cljs (extend-protocol IPrintWithWriter ValueRef (-pr-writer [vref writer _] (write-all writer (str "#flow-storm.types/value-ref " (:vid vref)))))) ================================================ FILE: src-shared/flow_storm/utils.cljc ================================================ (ns flow-storm.utils #?(:cljs (:require [goog.string :as gstr] [clojure.string :as str] [amalloy.ring-buffer :refer [ring-buffer]] [goog.string.format] [goog :as g]) :clj (:require [clojure.java.io :as io] [amalloy.ring-buffer :refer [ring-buffer]] [clojure.string :as str])) (:refer-clojure :exclude [format update-values update-keys]) #?(:clj (:import [java.io File LineNumberReader InputStreamReader PushbackReader] [clojure.lang RT IEditableCollection PersistentArrayMap PersistentHashMap] [java.util.logging Logger Level]))) (defn disable-from-profile [profile] (case profile :light #{:expr-exec :bind} #{})) (defn elide-string [s max-len] (let [len (count s)] (when (pos? len) (cond-> (subs s 0 (min max-len len)) (> len max-len) (str " ... "))))) #?(:clj (defn hash-map? [x] (or (instance? PersistentArrayMap x) (instance? PersistentHashMap x)))) (defn format [& args] #?(:clj (apply clojure.core/format args) :cljs (apply gstr/format args))) #?(:clj (defn colored-string [s c] (let [color {:red 31 :yellow 33}] (format "\033[%d;1;1m%s\033[0m" (color c) s))) :cljs (defn colored-string [_ _] "UNIMPLEMENTED")) (defn parse-int [s] #?(:clj (Integer/parseInt s) :cljs (js/parseInt s))) (defn str-coord->vec [str-coord] (if (str/blank? str-coord) [] (->> (str/split str-coord #",") (mapv (fn [x] (if (or (str/starts-with? x "K") (str/starts-with? x "V")) x (parse-int x))))))) #?(:clj (defn map-like? [x] (instance? java.util.Map x))) #?(:cljs (defn map-like? [x] (map? x))) #?(:clj (defn seq-like? [x] (instance? java.util.List x))) #?(:cljs (defn seq-like? [_] false)) #?(:cljs (defonce uuids (atom {:max-uuid 3 :strings-and-numbers {}}))) ;; copying goog.getUid https://github.com/google/closure-library/blob/master/closure/goog/base.js#L1306 #?(:cljs (def flow-storm-uuid-prop (str "flow_storm_" (unsigned-bit-shift-right (* (js/Math.random) 1e9) 0)))) #?(:clj (defn object-id [o] (System/identityHashCode o)) :cljs (defn object-id [o] (cond (or (undefined? o) (nil? o)) 0 (boolean? o) (if (true? o) 1 2) (or (number? o) (string? o)) (let [uuids' (swap! uuids (fn [{:keys [max-uuid strings-and-numbers] :as u}] (if (get strings-and-numbers o) u (let [next-uuid (inc max-uuid)] (-> u (assoc :max-uuid next-uuid) (update :strings-and-numbers assoc o next-uuid))))))] (get-in uuids' [:strings-and-numbers o])) (= "object" (g/typeOf o)) (or (and (js/Object.prototype.hasOwnProperty.call o flow-storm-uuid-prop) (aget o flow-storm-uuid-prop)) (let [next-uid (-> (swap! uuids update :max-uuid inc) :max-uuid)] (aset o flow-storm-uuid-prop next-uid)))))) #?(:clj (def logger (Logger/getLogger "flow_storm"))) #?(:clj (defn log [& msgs] (.log logger Level/INFO (apply str msgs))) :cljs (defn log [& msgs] (apply js/console.log msgs))) #?(:clj (defn log-error ([msg] (.log logger Level/WARNING msg)) ([msg ^Exception e] (.log logger Level/WARNING msg e))) :cljs (defn log-error ([msg] (js/console.error msg)) ([msg e] (js/console.error msg e)))) (defn rnd-uuid [] #?(:clj (java.util.UUID/randomUUID) :cljs (.-uuid ^cljs.core/UUID (random-uuid)))) (defn get-timestamp [] #?(:cljs (.now js/Date) :clj (System/currentTimeMillis))) (defn get-monotonic-timestamp [] #?(:cljs (.now js/Date) :clj (System/nanoTime))) (defn get-current-thread-id [] ;; TODO: eventually move this to (.threadId ...), since .getId was ;; deprecated and it looks like it is not reliable anymore #?(:clj (.getId (Thread/currentThread)) :cljs 0)) (defn get-current-thread-name [] #?(:clj (.getName (Thread/currentThread)) :cljs "main")) (defn get-thread-object-by-id [thread-id] ;; TODO: eventually move this to (.threadId ...), since .getId was ;; deprecated and it looks like it is not reliable anymore #?(:clj (some #(when (= (.getId %) thread-id) %) (.keySet (Thread/getAllStackTraces))) :cljs thread-id)) (defn get-memory-info [] #?(:clj {:max-heap-bytes (.maxMemory (Runtime/getRuntime)) :heap-size-bytes (.totalMemory (Runtime/getRuntime)) :heap-free-bytes (.freeMemory (Runtime/getRuntime))} :cljs {:max-heap-bytes 0 :heap-size-bytes 0 :heap-free-bytes 0})) (defn get-used-memory-bytes [] #?(:clj (- (.totalMemory (Runtime/getRuntime)) (.freeMemory (Runtime/getRuntime))) :cljs 0)) (defn get-used-memory-mb [] (some-> (get-used-memory-bytes) (quot 1048576))) (defn storm-env? [] #?(:clj (try (Class/forName "clojure.storm.Tracer") true (catch Exception _ false)) :cljs (boolean (try (js* "cljs.storm.tracer.trace_fn_call") (catch js/Error _ false))))) (defn flow-storm-nrepl-middleware? [] #?(:clj (boolean (find-ns 'flow-storm.nrepl.middleware)) :cljs false)) (defn ensure-vanilla [] (when (storm-env?) (throw (ex-info "Can't execute in a Storm environment. Use under Vanilla only." {})))) (defn ensure-storm [] (when-not (storm-env?) (throw (ex-info "Can't execute outside a Storm environment." {})))) (defn contains-only? [m ks] (empty? (apply dissoc m ks))) (defn merge-meta "Non-throwing version of (vary-meta obj merge metamap-1 metamap-2 ...). Like `vary-meta`, this only applies to immutable objects. For instance, this function does nothing on atoms, because the metadata of an `atom` is part of the atom itself and can only be changed destructively." {:style/indent 1} [obj & metamaps] (try (apply vary-meta obj merge metamaps) #?(:clj (catch Exception _ obj) :cljs (catch js/Error _ obj)))) (defn derefable? [x] #?(:clj (instance? clojure.lang.IDeref x) :cljs (instance? cljs.core.IDeref x))) #?(:clj (defn blocking-derefable? [x] (instance? clojure.lang.IBlockingDeref x)) :cljs (defn blocking-derefable? [_] false)) (defn pending? [x] #?(:clj (instance? clojure.lang.IPending x) :cljs (instance? cljs.core.IPending x))) #?(:clj (defn source-fn [x] (try (when-let [v (resolve x)] (when-let [filepath (:file (meta v))] (let [strm (or (.getResourceAsStream (RT/baseLoader) filepath) (io/input-stream filepath))] (when str (with-open [rdr (LineNumberReader. (InputStreamReader. strm))] (dotimes [_ (dec (:line (meta v)))] (.readLine rdr)) (let [text (StringBuilder.) pbr (proxy [PushbackReader] [rdr] (read [] (let [i (proxy-super read)] (.append text (char i)) i))) read-opts (if (.endsWith ^String filepath "cljc") {:read-cond :allow} {})] (if (= :unknown *read-eval*) (throw (IllegalStateException. "Unable to read source while *read-eval* is :unknown.")) (read read-opts (PushbackReader. pbr))) (str text)) ))))) (catch Exception _ nil)))) #?(:clj (defmacro deftype+ "Same as deftype, but: read mutable fields through ILookup: (:field instance)" [name fields & body] `(do (deftype ~name ~fields clojure.lang.ILookup (valAt [_# key# notFound#] (case key# ~@(mapcat #(vector (keyword %) %) fields) notFound#)) (valAt [this# key#] (.valAt this# key# nil))) (defn ~(symbol (str '-> name)) ~fields (new ~name ~@fields) ~@body))) ) #?(:clj (defmacro lazy-binding "Like clojure.core/binding but instead of a vec of vars it accepts a vec of symbols, and will resolve the vars with requiring-resolve" [bindings & body] (let [vars-binds (mapcat (fn [[var-symb var-val]] [`(clojure.core/requiring-resolve '~var-symb) var-val]) (partition 2 bindings))] `(let [] (push-thread-bindings (hash-map ~@vars-binds)) (try ~@body (finally (pop-thread-bindings))))))) #?(:clj (defn mk-tmp-dir! "Creates a unique temporary directory on the filesystem. Typically in /tmp on *NIX systems. Returns a File object pointing to the new directory. Raises an exception if the directory couldn't be created after 10000 tries." [] (let [base-dir (io/file (System/getProperty "java.io.tmpdir")) base-name (str (System/currentTimeMillis) "-" (long (rand 1000000000)) "-") tmp-base (doto (File. (str base-dir "/" base-name)) (.mkdir)) max-attempts 10000] (loop [num-attempts 1] (if (= num-attempts max-attempts) (throw (Exception. (str "Failed to create temporary directory after " max-attempts " attempts."))) (let [tmp-dir-name (str tmp-base num-attempts) tmp-dir (io/file tmp-dir-name)] (if (.mkdir tmp-dir) tmp-dir (recur (inc num-attempts))))))))) ;; So we don't depend on clojure 1.11 (defn update-values "m f => {k (f v) ...} Given a map m and a function f of 1-argument, returns a new map where the keys of m are mapped to result of applying f to the corresponding values of m." [m f] (with-meta (persistent! (reduce-kv (fn [acc k v] (assoc! acc k (f v))) (if (instance? IEditableCollection m) (transient m) (transient {})) m)) (meta m))) (defn update-keys "m f => {(f k) v ...} Given a map m and a function f of 1-argument, returns a new map whose keys are the result of applying f to the keys of m, mapped to the corresponding values of m. f must return a unique key for each key of m, else the behavior is undefined." {:added "1.11"} [m f] (let [ret (persistent! (reduce-kv (fn [acc k v] (assoc! acc (f k) v)) (transient {}) m))] (with-meta ret (meta m)))) #?(:clj (defn normalize-newlines [s] (-> s (.replaceAll "\\r\\n" "\n") (.replaceAll "\\r" "\n")))) #?(:clj (defn remove-newlines [s] (-> s normalize-newlines (.replaceAll "\\n" " ")))) #?(:clj (defmacro env-prop [prop-name] (System/getProperty prop-name))) (defn parse-thread-fn-call-limits [s] (when s (->> (str/split s #",") (mapv (fn [fn-desc] (let [[fqfn cnt] (str/split fn-desc #":") [fn-ns fn-name] (str/split fqfn #"/")] [fn-ns fn-name (parse-int cnt)])))))) (defn stringify-coord [coord-vec] (str/join "," coord-vec)) (defn lerp [from to t] (+ from (* t (- to from)))) (defn inverse-lerp [from to n] (float (/ (- n from) (- to from)))) (defn grep-coll "Search over coll with pred, when it matches returns the A elements before and the B elements after" [coll A B pred] (loop [elems-before (ring-buffer A) [e & rcoll] coll] (when e (if (pred e) {:before (into [] elems-before) :match e :after (into [] (take B rcoll))} (recur (conj elems-before e) rcoll))))) #?(:clj (defn call-jvm-method [class-name method-name args] (let [klass (Class/forName class-name) method (.getMethod klass method-name (into-array java.lang.Class (mapv class args)))] (.invoke method nil (into-array args))))) (defn quoted-string-split "Split string s with sep-char but don't split inside single quotes." [s sep-char] (loop [[c & rinput] s quote-on? false tokens [] curr-tok ""] (if-not c (conj tokens curr-tok) (cond (= c \') (recur rinput (not quote-on?) tokens curr-tok) (and (= c sep-char) (not quote-on?)) (recur rinput quote-on? (conj tokens curr-tok) "") :else (recur rinput quote-on? tokens (str curr-tok c)))))) (defn pop-n [stack n] (reduce (fn [st _] (pop st)) stack (range n))) #?(:clj (defn format-int [n radix] (if (int? n) (Long/toString (long n) radix) (throw (ex-info "Only integer (as by int?) numbers supported" {})))) :cljs (defn format-int [n radix] (if (int? n) (.toString n radix) (throw (ex-info "Only integer (as by int?) numbers supported" {}))))) #?(:clj (defn transient? [x] (or (instance? clojure.lang.ITransientVector x) (instance? clojure.lang.ATransientMap x) (instance? clojure.lang.ATransientSet x))) :cljs (defn transient? [x] (or (instance? cljs.core/TransientVector x) (instance? cljs.core/TransientArrayMap x) (instance? cljs.core/TransientHashSet x)))) ================================================ FILE: tests.edn ================================================ #kaocha/v1 {:tests [{:id :unit-clj :source-paths ["src-inst" "src-dbg" "src-shared"] :test-paths ["test"]}]}