{"id":18008202,"url":"https://github.com/dgrnbrg/spyscope","last_synced_at":"2025-04-08T09:06:31.450Z","repository":{"id":5147341,"uuid":"6314602","full_name":"dgrnbrg/spyscope","owner":"dgrnbrg","description":"Trace-oriented debugging tools for Clojure","archived":false,"fork":false,"pushed_at":"2019-12-25T12:22:15.000Z","size":39,"stargazers_count":589,"open_issues_count":10,"forks_count":35,"subscribers_count":19,"default_branch":"master","last_synced_at":"2025-04-01T07:45:32.233Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"WordPress/WordPress","license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dgrnbrg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-10-20T22:47:57.000Z","updated_at":"2025-01-25T14:31:59.000Z","dependencies_parsed_at":"2022-08-04T01:30:44.452Z","dependency_job_id":null,"html_url":"https://github.com/dgrnbrg/spyscope","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgrnbrg%2Fspyscope","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgrnbrg%2Fspyscope/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgrnbrg%2Fspyscope/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgrnbrg%2Fspyscope/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dgrnbrg","download_url":"https://codeload.github.com/dgrnbrg/spyscope/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247809964,"owners_count":20999816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-30T01:17:50.481Z","updated_at":"2025-04-08T09:06:31.420Z","avatar_url":"https://github.com/dgrnbrg.png","language":"Clojure","funding_links":[],"categories":["Libraries","Debugging"],"sub_categories":[],"readme":"# Spyscope\n\nA Clojure(Script) library designed to make it easy to debug single- and multi-threaded applications.\n\n## Installation\n\n\n#### Leiningen\n\nAdd `[spyscope \"0.1.6\"]` to your project.clj's `:dependencies`.\n\nIf you want spyscope to be automatically loaded and available in every project,\nadd the following to the `:user` profile in `~/.lein/profiles.clj`:\n\n    :dependencies [[spyscope \"0.1.6\"]]\n    :injections [(require 'spyscope.core)]\n\n#### Boot\n\nAfter requiring the namespace, you must also run `(boot.core/load-data-readers!)` \nto get the reader tags working. Using a `~/.boot/profile.boot` file:\n\n```\n(set-env! :dependencies #(conj % '[spyscope \"0.1.6\"]))\n\n(require 'spyscope.core)\n(boot.core/load-data-readers!)\n```\n\n## Usage\n\nSpyscope includes 3 reader tools for debugging your Clojure code, which are exposed as reader tags:\n`#spy/p`, `#spy/d`, and `#spy/t`, which stand for *print*, *details*, and *trace*, respectively.\nReader tags were chosen because they allow one to use Spyscope by only writing 6 characters, and\nsince they exist only to the left of the form one wants to debug, they require the fewest possible\nkeystrokes, optimizing for developer happiness. :)\n\n### `#spy/p`\n\nFirst, let's look at `#spy/p`, which just pretty-prints the form of interest:\n\n```clojure\nspyscope.repl=\u003e (take 20 (repeat #spy/p (+ 1 2 3)))\n6\n(6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)\n```\n\n`#spy/p` is an extremely simple tool that merely saves a few keystores when\none needs to dump out a value in the middle of a calculation.\n\n### `#spy/d`\n\nNext, let's look at `#spy/d`. This is where the real power lies:\n\n```clojure\nspyscope.repl=\u003e (take 20 (repeat #spy/d (+ 1 2 3)))\nspyscope.repl$eval3869.invoke(NO_SOURCE_FILE:1) (+ 1 2 3) =\u003e 6\n(6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)\n```\n\nIn the simplest usage, the form is printed along with the stack trace\nit occurred on, which makes it easier to grep through logs that have\nmany tracing statements enabled.\n\nOften, you may find that additional context would be beneficial.\nOne way to add context is to include a marker in all of the output.\nThis lets you add a semantic name to any spy:\n\n```clojure\nspyscope.repl=\u003e #spy/d ^{:marker \"triple-add\"} (+ 1 2 3)\nspyscope.repl$eval3935.invoke(NO_SOURCE_FILE:1) triple-add (+ 1 2 3) =\u003e 6\n6\n```\n\nIn addition, you can request additional stack frames with the\nmetadata key `:fs`, which gives you a richer context without you\ndoing anything:\n\naside: (`:fs` comes from first and last letters of \"frames\")\n\n```clojure\nspyscope.repl=\u003e (take 20 (repeat #spy/d ^{:fs 3} (+ 1 2 3)))\n----------------------------------------\nclojure.lang.Compiler.eval(Compiler.java:6477)\nclojure.lang.Compiler.eval(Compiler.java:6511)\nspyscope.repl$eval675.invoke(REPL:13) (+ 1 2 3) =\u003e 6\n(6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)\n```\n\nAs you can see, when multiple stack frames are printed, a row of dashes\nis printed before the trace to keep the start of the stack frame group\nclearly denoted.\n\nAs you debug further, you may realize that the context of the creation of\ncertain values is important; however, if you print out 10 or 20 lines of\nstack trace, you'll end up with an unreadable mess. The metadata key `:nses`\nallows you to apply a regex to the stacktrace frames to filter out noise:\n\n```clojure\nspyscope.repl=\u003e (take 20 (repeat #spy/d ^{:fs 3 :nses #\"core|spyscope\"} (+ 1 2 3)))\n----------------------------------------\nclojure.core$apply.invoke(core.clj:601)\nclojure.core$eval.invoke(core.clj:2797)\nspyscope.repl$eval678.invoke(REPL:14) (+ 1 2 3) =\u003e 6\n(6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)\n```\n\nIf you leave your application unattended for a period of time, you may\nwish to have timestamps included in all the output lines. Spyscope can use\na default time format, or a user-provided one:\n\n```clojure\n;; Default formatter is yyyy-mm-ddThh:mm:ss\nspyscope.repl=\u003e #spy/d ^{:time true} (+ 1 2 3)\nspyscope.repl$eval4028.invoke(NO_SOURCE_FILE:1) 2013-04-11T03:20:46 (+ 1 2 3) =\u003e 6\n6\n```\n\n```clojure\n;; Custom formatters use clj-time\nspyscope.repl=\u003e #spy/d ^{:time \"hh:mm:ss\"} (+ 1 2 3)\nspyscope.repl$eval4061.invoke(NO_SOURCE_FILE:1) 03:21:40 (+ 1 2 3) =\u003e 6\n6\n```\n\nThe last feature of `#spy/d` is that it can suppress printing the code\nthat generated the value, which can be used to de-clutter the output\nif you have particularly large forms. This is controlled by setting\nthe metadata key `:form` to `false`:\n\n```clojure\nspyscope.repl=\u003e {:a #spy/d ^{:form false} (+ 1 2 3)\n                 :b #spy/d ^{:form false} (- 16 10)}\nspyscope.repl$eval685.invoke(REPL:16) =\u003e 6\nspyscope.repl$eval685.invoke(REPL:16) =\u003e 6\n{:a 6, :b 6}\n```\n\nUnder the hood, `#spy/d` actually does all of its printing on another thread\n--the tracing store thread! This provides 2 benefits: if you are printing\nfrom multiple threads, your output will not be interleaved amongst threads. The\nother benefit is that every trace statement is logged, so that you can use\nthe `#spy/t` api to refine your search after you start tracing with `#spy/d`.\n\n### `#spy/t`\n\nFinally, let's look at `#spy/t`. Tracing is very similar to detailed\nprinting, but it enables us to get meaningful results when using `#spy/d`\non a program that has multiple interacting threads without affecting\nmost interactive development workflows!\n\n`#spy/t` accepts all of the metadata arguments that `#spy/d` does (i.e.\n`:fs`, `:nses`, and `:form`).\n\nInstead of immediately printing out results, it stores them in an\nagent asynchronously. Each time a trace is logged, it is placed into\nthe current generation. One can use a function to increment the generation\ncounter, and previous generations are stored, so that one can compare\nseveral recent generations to understand what effects changes may have had.\n\nThere are several functions you can use to interact with the trace store:\n\n* `trace-query` is the workhorse function. With no arguments, it prints every\ntrace from the current generation. With a numeric argument `generations`,\nit prints every trace from the past `generations` generations. With a\nregex argument `re`, it prints every trace from the current generation whose\nroot stack frame matches the regex. Also accepts 2 arguments to specify the\nfiltering regex and how many generations to include.\n\n* `trace-next` moves onto the next generation. One usually calls this between\ntrials or experiments.\n\n* `trace-clear` deletes all trace data collected so far. Since all trace\ndata is saved, that can become quite a lot of data, so this can be used\nto clean up very long running sessions.\n\n## Example annotated `#spy/t` session\n\n```clojure\n;;Let's run some code on futures, but see the chronological result\nuser=\u003e (future (Thread/sleep 1000) #spy/t ^{:form false} (+ 1 2))\n       (future #spy/t (+ 3 4))\n#\u003cFuture@1013d7df: :pending\u003e\n;;We'll need to use the repl functions\nuser=\u003e (use 'spyscope.repl)\nnil\n;;trace-query shows all the traces by default, separated by dashed lines\nuser=\u003e (trace-query)\nuser$eval35677$fn__35689.invoke(NO_SOURCE_FILE:1) (+ 3 4) =\u003e 7\n----------------------------------------\nuser$eval35677$fn__35678.invoke(NO_SOURCE_FILE:1) =\u003e 3\nnil\n;;We'll define and invoke a function with a #spy/t\nuser=\u003e (defn my-best-fn [] #spy/t ^{:form false} (* 5 6))\n       (my-best-fn)\n30 ;Here's the return value--note that the trace isn't printed\n;;Let's see all traces so far \nuser=\u003e (trace-query)\nuser$eval35677$fn__35689.invoke(NO_SOURCE_FILE:1) (+ 3 4) =\u003e 7\n----------------------------------------\nuser$eval35677$fn__35678.invoke(NO_SOURCE_FILE:1) =\u003e 3\n----------------------------------------\nuser$eval35822$my_best_fn__35823.invoke(NO_SOURCE_FILE:1) =\u003e 30 ;Here's our new trace\nnil\n;;We can use a filter regex to only see matching stack frames \n;;Usually, you can filter by the name of the innermost function\n;;You can increase the :fs metadata parameter to have more context to filter by\nuser=\u003e (trace-query #\"best\")\nuser$eval35822$my_best_fn__35823.invoke(NO_SOURCE_FILE:1) =\u003e 30\nnil\n;;Move onto a new generation\nuser=\u003e (trace-next)\nnil\n;;No traces in this generation\nuser=\u003e (trace-query)\nnil\n;;Increase the number of generations in the query to review older traces\nuser=\u003e (trace-query 2)\nuser$eval35677$fn__35689.invoke(NO_SOURCE_FILE:1) (+ 3 4) =\u003e 7\n----------------------------------------\nuser$eval35677$fn__35678.invoke(NO_SOURCE_FILE:1) =\u003e 3\n----------------------------------------\nuser$eval35822$my_best_fn__35823.invoke(NO_SOURCE_FILE:1) =\u003e 30\nnil\n;;Add a new trace to the current generation\nuser=\u003e (my-best-fn)\n30\n;;We can see that there's only one trace in this generation--the one we just made\nuser=\u003e (trace-query)\nuser$eval35822$my_best_fn__35823.invoke(NO_SOURCE_FILE:1) =\u003e 30\nnil\n;;We can combine the generation and regex filter to search and filter many generations\n;;Here we see the invocations of my-best-fn from the current and previous generation\nuser=\u003e (trace-query #\"best\" 2)\nuser$eval35822$my_best_fn__35823.invoke(NO_SOURCE_FILE:1) =\u003e 30\n----------------------------------------\nuser$eval35822$my_best_fn__35823.invoke(NO_SOURCE_FILE:1) =\u003e 30\nnil\nuser=\u003e \n```\n\n## Contributors\n\nDavid Greenberg (@dgrnbrg) and Herwig Hochleitner (@bendlas)\n\n## License\n\nCopyright © 2012 David Greenberg\n\nDistributed under the Eclipse Public License, the same as Clojure.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgrnbrg%2Fspyscope","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdgrnbrg%2Fspyscope","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgrnbrg%2Fspyscope/lists"}