diff --git a/tools/viztrace/Manifest.toml b/tools/viztrace/Manifest.toml new file mode 100644 index 0000000..b0c4213 --- /dev/null +++ b/tools/viztrace/Manifest.toml @@ -0,0 +1,41 @@ +# This file is machine-generated - editing it directly is not advised + +[[ArgParse]] +deps = ["Logging", "TextWrap"] +git-tree-sha1 = "4a8f4df432fd8e8a96a142c53f9432b9022a92e6" +uuid = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +version = "1.1.1" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.1" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[Parsers]] +deps = ["Dates"] +git-tree-sha1 = "50c9a9ed8c714945e01cd53a21007ed3865ed714" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.0.15" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[TextWrap]] +git-tree-sha1 = "9250ef9b01b66667380cf3275b3f7488d0e25faf" +uuid = "b718987f-49a8-5099-9789-dcd902bef87d" +version = "1.0.1" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/tools/viztrace/Project.toml b/tools/viztrace/Project.toml new file mode 100644 index 0000000..f0ba8ee --- /dev/null +++ b/tools/viztrace/Project.toml @@ -0,0 +1,3 @@ +[deps] +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" diff --git a/tools/viztrace/README.md b/tools/viztrace/README.md new file mode 100644 index 0000000..9ffcd4b --- /dev/null +++ b/tools/viztrace/README.md @@ -0,0 +1,67 @@ +# VizTrace +**Trace visualization tool for UnetStack JSON trace files** + +UnetStack 3.3 introduced the event logging framework and JSON trace file format that contains detailed information for automated analysis of network traces. To illustrate the power of the event logging framework, we have built a simple viztrace tool to automatically draw sequence diagrams from the JSON trace files. The tool is written in Julia, and will require a working installation of [Julia](https://julialang.org/downloads/) on your machine to run. + +To illustrate the power of the tool, let us simulate a simple [2-node network](https://unetstack.net/handbook/unet-handbook_getting_started.html) and make a range measurment from node A to B. On node A: + +``` +> range host('B') +999.99976 +``` + +If you look in the `logs` folder in the simulator, you'll find a `trace.nam` file. We can analyze it using the `viztrace` tool: + +```sh +$ julia --project viztrace.jl trace.json +Specify a trace: +1: 1617881734525 [B] AddressAllocReq ⟦ node → arp ⟧ (1 events) +2: 1617881734525 [A] AddressAllocReq ⟦ node → arp ⟧ (1 events) +3: 1617881734531 [A] AddressResolutionReq ⟦ websh → arp ⟧ (1 events) +4: 1617881734595 [A] RangeReq ⟦ websh → ranging ⟧ (23 events) +``` + +So the tool tells us that there are 4 event traces in the `trace.json` file. The first 2 traces are related to address allocations on nodes B and A. The third trace is an address resolution for node B, when we called `host('B')`. The final trace is the actual ranging event, consisting of 23 individual sub-events. Let's explore that in more detail: + +```sh +$ julia --project viztrace.jl -t 4 trace.json > event4.mmd +``` + +This generates a [mermaid](https://mermaid-js.github.io/) sequence diagram for all the events in trace 4: + +``` +sequenceDiagram + participant websh_A as websh/A + participant ranging_A as ranging/A + participant mac_A as mac/A + participant phy_A as phy/A + participant phy_B as phy/B + participant ranging_B as ranging/B + websh_A->>ranging_A: RangeReq + ranging_A-->>websh_A: AGREE + ranging_A->>mac_A: ReservationReq + mac_A->>ranging_A: ReservationRsp + mac_A->>ranging_A: ReservationStatusNtf + ranging_A->>phy_A: ClearReq + phy_A-->>ranging_A: AGREE + ranging_A->>phy_A: TxFrameReq + phy_A-->>ranging_A: AGREE + phy_A->>ranging_A: TxFrameNtf + phy_A->>phy_B: HalfDuplexModem$TX + phy_B->>ranging_B: RxFrameNtf + ranging_B->>phy_B: ClearReq + phy_B-->>ranging_B: AGREE + ranging_B->>phy_B: TxFrameReq + phy_B-->>ranging_B: AGREE + phy_B->>ranging_B: TxFrameNtf + phy_B->>phy_A: HalfDuplexModem$TX + phy_A->>ranging_A: RxFrameNtf + ranging_A->>websh_A: RangeNtf + ranging_A->>mac_A: ReservationCancelReq + mac_A-->>ranging_A: AGREE + mac_A->>ranging_A: ReservationStatusNtf +``` + +We can easily convert this to a nice sequence diagram using the [mermaid command-line interface](https://github.com/mermaid-js/mermaid-cli) or the [mermaid online live editor](https://mermaid-js.github.io/mermaid-live-editor): + +![](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IHdlYnNoX0EgYXMgd2Vic2gvQVxuICBwYXJ0aWNpcGFudCByYW5naW5nX0EgYXMgcmFuZ2luZy9BXG4gIHBhcnRpY2lwYW50IG1hY19BIGFzIG1hYy9BXG4gIHBhcnRpY2lwYW50IHBoeV9BIGFzIHBoeS9BXG4gIHBhcnRpY2lwYW50IHBoeV9CIGFzIHBoeS9CXG4gIHBhcnRpY2lwYW50IHJhbmdpbmdfQiBhcyByYW5naW5nL0JcbiAgd2Vic2hfQS0-PnJhbmdpbmdfQTogUmFuZ2VSZXFcbiAgcmFuZ2luZ19BLS0-PndlYnNoX0E6IEFHUkVFXG4gIHJhbmdpbmdfQS0-Pm1hY19BOiBSZXNlcnZhdGlvblJlcVxuICBtYWNfQS0-PnJhbmdpbmdfQTogUmVzZXJ2YXRpb25Sc3BcbiAgbWFjX0EtPj5yYW5naW5nX0E6IFJlc2VydmF0aW9uU3RhdHVzTnRmXG4gIHJhbmdpbmdfQS0-PnBoeV9BOiBDbGVhclJlcVxuICBwaHlfQS0tPj5yYW5naW5nX0E6IEFHUkVFXG4gIHJhbmdpbmdfQS0-PnBoeV9BOiBUeEZyYW1lUmVxXG4gIHBoeV9BLS0-PnJhbmdpbmdfQTogQUdSRUVcbiAgcGh5X0EtPj5yYW5naW5nX0E6IFR4RnJhbWVOdGZcbiAgcGh5X0EtPj5waHlfQjogSGFsZkR1cGxleE1vZGVtJFRYXG4gIHBoeV9CLT4-cmFuZ2luZ19COiBSeEZyYW1lTnRmXG4gIHJhbmdpbmdfQi0-PnBoeV9COiBDbGVhclJlcVxuICBwaHlfQi0tPj5yYW5naW5nX0I6IEFHUkVFXG4gIHJhbmdpbmdfQi0-PnBoeV9COiBUeEZyYW1lUmVxXG4gIHBoeV9CLS0-PnJhbmdpbmdfQjogQUdSRUVcbiAgcGh5X0ItPj5yYW5naW5nX0I6IFR4RnJhbWVOdGZcbiAgcGh5X0ItPj5waHlfQTogSGFsZkR1cGxleE1vZGVtJFRYXG4gIHBoeV9BLT4-cmFuZ2luZ19BOiBSeEZyYW1lTnRmXG4gIHJhbmdpbmdfQS0-PndlYnNoX0E6IFJhbmdlTnRmXG4gIHJhbmdpbmdfQS0-Pm1hY19BOiBSZXNlcnZhdGlvbkNhbmNlbFJlcVxuICBtYWNfQS0tPj5yYW5naW5nX0E6IEFHUkVFXG4gIG1hY19BLT4-cmFuZ2luZ19BOiBSZXNlcnZhdGlvblN0YXR1c050ZlxuIiwibWVybWFpZCI6e30sInVwZGF0ZUVkaXRvciI6ZmFsc2V9) diff --git a/tools/viztrace/viztrace.jl b/tools/viztrace/viztrace.jl new file mode 100644 index 0000000..4e6811c --- /dev/null +++ b/tools/viztrace/viztrace.jl @@ -0,0 +1,213 @@ +import Pkg +Pkg.instantiate() + +using ArgParse +import JSON + +### analysis functions + +function groups(data; init=Tuple{String,Any}[]) + if "events" ∈ keys(data) + events = data["events"] + n = length(init) + for e ∈ events + groups(e; init) + end + length(init) == n && push!(init, (data["group"], events)) + end + init +end + +function findtrace(cache, ev) + if "threadID" ∈ keys(ev) + id = ev["threadID"] + id ∈ keys(cache) && return cache[id] + end + if "stimulus" ∈ keys(ev) + id = ev["stimulus"]["messageID"] + id ∈ keys(cache) && return cache[id] + end + 0 +end + +function addtocache!(cache, ev, trace) + if "threadID" ∈ keys(ev) + id = ev["threadID"] + cache[id] = trace + end + if "stimulus" ∈ keys(ev) + id = ev["stimulus"]["messageID"] + cache[id] = trace + end + if "response" ∈ keys(ev) + id = ev["response"]["messageID"] + cache[id] = trace + end +end + +function splitcomponent(s) + m = match(r"^(?.*)::(?[^/]*)/?(?.*)$", s) + m === nothing && return "", "", "" + m[:name], m[:clazz], m[:node] +end + +function prettymessage(msg) + clazz = msg["clazz"] + p = findlast('.', clazz) + p === nothing || (clazz = clazz[p+1:end]) + sender = "sender" ∈ keys(msg) ? msg["sender"] : "" + recipient = "recipient" ∈ keys(msg) ? msg["recipient"] : "" + "$clazz ⟦ $sender → $recipient ⟧" +end + +function analyze(events) + traces = Tuple{Int64,String,Vector{Any}}[] + cache = Dict{String,Int64}() + for ev ∈ events + trace = findtrace(cache, ev) + if trace == 0 + cname, cclazz, cnode = splitcomponent(ev["component"]) + cnode != "" && (cnode = "[$cnode] ") + desc = cnode * cname + if "stimulus" ∈ keys(ev) + desc = cnode * prettymessage(ev["stimulus"]) + elseif "response" ∈ keys(ev) + desc = cnode * prettymessage(ev["response"]) + end + push!(traces, (ev["time"], desc, [ev])) + trace = length(traces) + else + push!(traces[trace][3], ev) + end + addtocache!(cache, ev, trace) + end + traces +end + +function capture!(actors, msgs, origin, node, agent, t, msg, stimulus) + "recipient" ∉ keys(msg) && return + recipient = msg["recipient"] + if startswith(recipient, '#') + stimulus || return + recipient = agent + end + onode = node + okey = msg["messageID"] * "→" * recipient + if okey ∈ keys(origin) + n = origin[okey] + onode == n && return + onode = n + end + stimulus && "sender" ∉ keys(msg) && return + sender = "sender" ∈ keys(msg) ? msg["sender"] : agent + if node != "" + sender = sender * "/" * onode + recipient = recipient * "/" * node + end + clazz = msg["clazz"] + if clazz == "org.arl.fjage.Message" + clazz = msg["performative"] + else + p = findlast('.', clazz) + p === nothing || (clazz = clazz[p+1:end]) + end + origin[okey] = node + sender == recipient && return + push!(actors, (onode, sender)) + push!(actors, (node, recipient)) + push!(msgs, (t, clazz, sender, recipient)) +end + +function sequence(events) + actors = Tuple{String,String}[] + msgs = Tuple{Int64,String,String,String}[] + origin = Dict{String,String}() + for ev ∈ events + cname, cclazz, cnode = splitcomponent(ev["component"]) + "stimulus" ∈ keys(ev) && capture!(actors, msgs, origin, cnode, cname, ev["time"], ev["stimulus"], true) + "response" ∈ keys(ev) && capture!(actors, msgs, origin, cnode, cname, ev["time"], ev["response"], false) + end + unique!(sort!(actors; by = x -> x[1])) + actors, msgs +end + +function mermaid(actors, msgs) + println("sequenceDiagram") + for a ∈ actors + id = replace(a[2], '/' => '_') + println(" participant $id as $(a[2])") + end + for m ∈ msgs + id1 = replace(m[3], '/' => '_') + id2 = replace(m[4], '/' => '_') + print(" $id1") + m[2] == "AGREE" && print('-') + println("->>$id2: $(m[2])") + end +end + +### main + +aps = ArgParseSettings() +@add_arg_table! aps begin + "--group", "-g" + help = "event group number" + arg_type = Int + default = 0 + "--trace", "-t" + help = "trace number" + arg_type = Int + default = 0 + "filename" + help = "JSON trace file" + default = "trace.json" +end +args = parse_args(ARGS, aps) + +if !isfile(args["filename"]) + println(args["filename"], " not found") + exit(1) +end + +grps = groups(JSON.parsefile(args["filename"])) + +if length(grps) < 1 + println("no traces found") + exit(1) +end + +g = args["group"] +length(grps) == 1 && g == 0 && (g = 1) + +if g == 0 + println("Specify an event group:") + for (i, g1) ∈ enumerate(grps) + println(i, ": ", g1[1], " (", length(g1[2]), " events)") + end + exit(1) +end + +if g < 1 || length(grps) < g + println("invalid event group number") + exit(1) +end + +traces = analyze(grps[g][2]) + +t = args["trace"] +length(traces) == 1 && t == 0 && (t = 1) + +if t == 0 + println("Specify a trace:") + for (i, t1) ∈ enumerate(traces) + println(i, ": ", t1[1], " ", t1[2], " (", length(t1[3]), " events)") + end + exit(1) +end + +if t < 1 || length(traces) < t + println("invalid trace number") + exit(1) +end + +mermaid(sequence(traces[t][3])...) diff --git a/unetsocket/c/unet.c b/unetsocket/c/unet.c index 74b0622..85cee19 100644 --- a/unetsocket/c/unet.c +++ b/unetsocket/c/unet.c @@ -1,4 +1,4 @@ -#define _BSD_SOURCE +#define _DEFAULT_SOURCE #include #include #include "pthreadwindows.h" diff --git a/unetsocket/python/unetpy/__init__.py b/unetsocket/python/unetpy/__init__.py index 0d0fbf0..bddacff 100644 --- a/unetsocket/python/unetpy/__init__.py +++ b/unetsocket/python/unetpy/__init__.py @@ -391,6 +391,7 @@ def receive(self): def cancel(self): """Cancels an ongoing blocking receive().""" if self.waiting: + self.gw.cancel = True self.gw.cv.acquire() self.gw.cv.notify() self.gw.cv.release()