├── .github
└── ISSUE_TEMPLATE
│ ├── bug-report-in-live-version.md
│ └── feature_request.md
├── LICENSE
├── README.md
└── visualizations
├── .gitignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── public
├── index.html
└── standalone_data
│ ├── draft-00
│ ├── example_github.qlog.js
│ └── quictrace_example_github.qlog
│ ├── draft-01
│ ├── doublevantage_100ms.qlog
│ ├── new_cid.qlog
│ ├── new_cid.qlog.js
│ ├── parallel_10_50KB_f5.qlog
│ ├── spin_bit.qlog
│ └── spin_bit.qlog.js
│ ├── prespec
│ ├── ngtcp2_multistreams_server_10ploss.qlog.js
│ ├── ngtcp2_multistreams_server_losscomparison.qlog.js
│ ├── ngtcp2_multistreams_server_noloss.qlog.js
│ ├── ngtcp2_pcap1.qlog.js
│ └── quictracker_handshake_v6_quicker_20181219.qlog.js
│ └── tcp
│ └── wikipedia_Playstation.json
├── src
├── App.vue
├── components
│ ├── HelloWorld.vue
│ ├── congestiongraph
│ │ ├── CongestionGraphConfigurator.vue
│ │ ├── CongestionGraphContainer.vue
│ │ ├── CongestionGraphRenderer.vue
│ │ ├── data
│ │ │ └── CongestionGraphConfig.ts
│ │ └── renderer
│ │ │ ├── CongestionGraphD3Renderer.ts
│ │ │ ├── MainGraphState.ts
│ │ │ └── RecoveryGraphState.ts
│ ├── filemanager
│ │ ├── FileManagerContainer.vue
│ │ ├── data
│ │ │ └── FileLoader.ts
│ │ ├── netlogconverter
│ │ │ ├── netlog.ts
│ │ │ └── netlogtoqlog.ts
│ │ ├── newlineconverter
│ │ │ ├── newlinejsontoqlog.ts
│ │ │ └── textsequencejsontoqlog.ts
│ │ ├── pcapconverter
│ │ │ ├── qlog_tcp_tls_h2.ts
│ │ │ └── tcptoqlog.ts
│ │ └── utils
│ │ │ └── StreamingJSONParser.ts
│ ├── multiplexinggraph
│ │ ├── MultiplexingGraphCollapsedRenderer.vue
│ │ ├── MultiplexingGraphConfigurator.vue
│ │ ├── MultiplexingGraphContainer.vue
│ │ ├── MultiplexingGraphRenderer.vue
│ │ ├── data
│ │ │ └── MultiplexingGraphConfig.ts
│ │ └── renderer
│ │ │ ├── MultiplexingGraphD3ByterangesRenderer.ts
│ │ │ ├── MultiplexingGraphD3CollapsedRenderer.ts
│ │ │ ├── MultiplexingGraphD3SimulationRenderer.ts
│ │ │ ├── MultiplexingGraphD3WaterfallRenderer.ts
│ │ │ └── MultiplexingGraphDataHelper.ts
│ ├── packetizationdiagram
│ │ ├── PacketizationDiagramConfigurator.vue
│ │ ├── PacketizationDiagramContainer.vue
│ │ ├── PacketizationDiagramRenderer.vue
│ │ ├── data
│ │ │ └── PacketizationDiagramConfig.ts
│ │ └── renderer
│ │ │ ├── PacketizationDiagramD3Renderer.ts
│ │ │ ├── PacketizationDiagramDataHelper.ts
│ │ │ ├── PacketizationDiagramModels.ts
│ │ │ ├── PacketizationQUICPreProcessor.ts
│ │ │ └── PacketizationTCPPreProcessor.ts
│ ├── sequencediagram
│ │ ├── SequenceDiagramConfigurator.vue
│ │ ├── SequenceDiagramContainer.vue
│ │ ├── SequenceDiagramRenderer.vue
│ │ ├── SequenceDiagramSimpleRenderer.vue
│ │ ├── data
│ │ │ └── SequenceDiagramConfig.ts
│ │ └── renderer
│ │ │ ├── SequenceDiagramCanvasRenderer.ts
│ │ │ └── SequenceDiagramD3Renderer.ts
│ ├── shared
│ │ ├── ConnectionConfigurator.vue
│ │ └── helpers
│ │ │ └── ColorHelper.ts
│ └── stats
│ │ ├── StatisticsConfigurator.vue
│ │ ├── StatisticsConnectionRenderer.vue
│ │ ├── StatisticsContainer.vue
│ │ ├── StatisticsRenderer.vue
│ │ └── data
│ │ └── StatisticsConfig.ts
├── data
│ ├── Connection.ts
│ ├── ConnectionGroup.ts
│ ├── QlogEventParser.ts
│ ├── QlogLoader.ts
│ ├── QlogLoaderV2.ts
│ ├── QlogSchema.ts
│ ├── QlogSchema01.ts
│ ├── QlogSchema02.ts
│ └── QlogSchemaConverter.ts
├── main.ts
├── router.ts
├── store.ts
├── store
│ ├── ConfigurationStore.ts
│ └── ConnectionStore.ts
├── types
│ ├── oboe.d.ts
│ ├── shims-tsx.d.ts
│ └── shims-vue.d.ts
└── views
│ ├── CongestionGraph.vue
│ ├── FileManager.vue
│ ├── MainMenu.vue
│ ├── MultiplexingGraph.vue
│ ├── PacketizationDiagram.vue
│ ├── SequenceDiagram.vue
│ ├── Statistics.vue
│ └── VUEDebug.vue
├── tsconfig.json
├── tslint.json
└── vue.config.js
/.github/ISSUE_TEMPLATE/bug-report-in-live-version.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report in live version
3 | about: Report a bug in the live version of qvis
4 | title: Bug in live version
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | - How did you load the file?
15 | - Which visualization had the error?
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Additional context**
24 | If you are having problems loading a file, please attach it to this issue or email it to us at robin.marx@uhasselt.be
25 |
26 | Note that qvis is currently only tested and maintained for the latest stable versions of Google Chrome.
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Hasselt University - EDM
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qvis
2 |
3 | A set of QUIC and HTTP/3 visualization tools.
4 |
5 | The tools are mainly expected to be used with the [qlog logging format](https://github.com/quicwg/qlog/) as input,
6 | but we also have (partial) support for uploading pcap files,
7 | as well as Google Chrome netlog files.
8 |
9 | A full-featured, hosted version with example qlog files can be found at https://qvis.quictools.info/.
10 | Instructions and docker files for setting up your own copy can be found at https://github.com/quiclog/qvis-server.
11 |
12 |
13 | ## older versions
14 |
15 | This is the new version of the qvis visualization suite.
16 | The old version can be found at https://github.com/rmarx/quicvis
17 | The results from the paper ["Towards QUIC Debuggability"](https://quic.edm.uhasselt.be/) were obtained using the old version.
18 | The old version is no longer maintained and is not compatible with the new qlog formats (draft-01+).
19 |
20 | Results from [newer papers](https://qlog.edm.uhasselt.be/) were all obtained using (variations on) tools in the current qvis toolsuite.
21 |
--------------------------------------------------------------------------------
/visualizations/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /public/standalone_data/draft-00/mvfst_large.qlog
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 |
--------------------------------------------------------------------------------
/visualizations/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4
3 | }
--------------------------------------------------------------------------------
/visualizations/README.md:
--------------------------------------------------------------------------------
1 | # src
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Run your tests
19 | ```
20 | npm run test
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | npm run lint
26 | ```
27 |
28 | ### Customize configuration
29 | See [Configuration Reference](https://cli.vuejs.org/config/).
30 |
--------------------------------------------------------------------------------
/visualizations/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qvis",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@quictools/qlog-schema": ">=0.0.16",
12 | "@types/d3": "^5.7.2",
13 | "axios": "^0.18.1",
14 | "bootstrap-vue": "^2.0.4",
15 | "d3": "^5.12.0",
16 | "oboe": "^2.1.5",
17 | "vue": "^2.6.10",
18 | "vue-class-component": "^6.0.0",
19 | "vue-notification": "^1.3.16",
20 | "vue-property-decorator": "^7.3.0",
21 | "vue-router": "^3.1.3",
22 | "vuex": "^3.1.1",
23 | "vuex-module-decorators": "^0.9.11"
24 | },
25 | "devDependencies": {
26 | "@vue/cli-plugin-typescript": "^3.12.1",
27 | "@vue/cli-service": "^3.12.1",
28 | "typescript": "^3.6.4",
29 | "vue-template-compiler": "^2.6.10"
30 | },
31 | "postcss": {
32 | "plugins": {
33 | "autoprefixer": {}
34 | }
35 | },
36 | "browserslist": [
37 | "> 1%",
38 | "last 2 versions",
39 | "not ie <= 8"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/visualizations/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | qvis: tools and visualizations for QUIC and HTTP/3
9 |
10 |
11 |
12 | We're sorry but qvis doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
34 |
35 |
36 |
60 |
--------------------------------------------------------------------------------
/visualizations/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/visualizations/src/components/congestiongraph/CongestionGraphConfigurator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Select a trace via the dropdown(s) below to visualize it in the congestion graph
7 |
8 |
9 |
10 |
11 |
12 | Reset zoom
13 | Zoom timerange
14 | Zoom area
15 | Ruler (press R)
16 | Toggle congestion info
17 | Toggle RTT zooming
18 |
19 |
20 |
21 | Please load a trace file to visualize it
22 | Loading files...
23 |
24 |
25 |
26 |
27 |
28 |
29 |
38 |
39 |
141 |
--------------------------------------------------------------------------------
/visualizations/src/components/congestiongraph/CongestionGraphContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
46 |
--------------------------------------------------------------------------------
/visualizations/src/components/congestiongraph/CongestionGraphRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | : Data sent (includes retransmits)
9 |
10 |
11 |
12 | : Data acknowledged
13 |
14 |
15 |
16 | : Data lost
17 |
18 |
19 |
20 |
21 | : Connection flow control limit
22 |
23 |
24 |
25 |
26 | : Sum of stream flow control limits
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | : Congestion window
35 |
36 |
37 |
38 | : Bytes in flight
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | : Min RTT
49 |
50 |
51 |
52 |
53 | : Latest RTT
54 |
55 |
56 |
57 | : Smoothed RTT
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
81 |
82 |
132 |
--------------------------------------------------------------------------------
/visualizations/src/components/congestiongraph/data/CongestionGraphConfig.ts:
--------------------------------------------------------------------------------
1 | import Connection from "@/data/Connection";
2 | import CongestionGraphD3Renderer from '../renderer/CongestionGraphD3Renderer';
3 |
4 | export default class CongestionGraphConfig {
5 | // PROPERTIES MUST BE INITIALISED
6 | // OTHERWISE VUE DOES NOT MAKE THEM REACTIVE
7 | // !!!!!
8 |
9 | public connection:Connection | undefined = undefined;
10 | public renderer!: CongestionGraphD3Renderer; // ONLY HERE FOR DEBUGGING, PROPERTY IS NOT INITIALISED ON PURPOSE SO THAT IT IS NOT MADE REACTIVE
11 | }
12 |
--------------------------------------------------------------------------------
/visualizations/src/components/congestiongraph/renderer/MainGraphState.ts:
--------------------------------------------------------------------------------
1 | import { ScaleLinear, Axis, Selection, BaseType, BrushBehavior } from "d3";
2 |
3 | export class MainGraphState {
4 | /* Fields */
5 | public eventBus: HTMLSpanElement | null = null; // A dummy DOM element which is used to fire off custom events
6 | /*
7 | Events:
8 | - packetSelectionEvent
9 | - packetPickEvent
10 | */
11 |
12 | public outerWidth = window.innerWidth;
13 | public outerHeight = 600;
14 | public margins = {
15 | top: 20,
16 | bottom: 60,
17 | left: 70,
18 | right: 70,
19 | };
20 | public innerWidth: number;
21 | public innerHeight: number;
22 |
23 | public graphSvg: Selection | null = null;
24 | public canvas: Selection | null = null;
25 | public canvasContext: CanvasRenderingContext2D | null = null;
26 | public mouseHandlerPanningSvg: Selection | null = null;
27 | public mouseHandlerBrushXSvg: Selection | null = null;
28 | public mouseHandlerBrush2dSvg: Selection | null = null;
29 | public mouseHandlerSelectionSvg: Selection | null = null;
30 | public mouseHandlerPickSvg: Selection | null = null;
31 | public mouseHandlerRulerSvg: Selection | null = null;
32 | public brushX: BrushBehavior | null = null;
33 | public brushXElement: Selection | null = null;
34 | public brush2d: BrushBehavior | null = null;
35 | public brush2dElement: Selection | null = null;
36 | public selectionBrush: BrushBehavior | null = null;
37 | public packetInformationDiv: Selection | null = null;
38 | public congestionGraphEnabled = true;
39 |
40 | public useSentPerspective = true;
41 |
42 | public gxAxis: Selection | null = null;
43 | public gyAxis: Selection | null = null;
44 | public gyCongestionAxis: Selection | null = null;
45 |
46 | public congestionAxisText: Selection | null = null;
47 |
48 | // Perspective in which packet_sent events play the main role
49 | public sent!: IPerspectiveInfo;
50 |
51 | // Perspective in which packet_received events play the main role
52 | // As recovery/congestion information is not available from this perspective, it is more limited than the 'sent' perspective
53 | public received!: IPerspectiveInfo;
54 |
55 | public metricUpdateLines!: {
56 | bytes: Array<[number, number]>,
57 | cwnd: Array<[number, number]>,
58 | minRTT: Array<[number, number]>,
59 | smoothedRTT: Array<[number, number]>,
60 | lastRTT: Array<[number, number]>,
61 | };
62 |
63 | public flowControlLines!: {
64 | application: Array<[number, number]>,
65 | stream: Array<[number, number]>,
66 | }
67 |
68 | /* Methods */
69 | public constructor() {
70 | this.innerWidth = this.outerWidth - this.margins.left - this.margins.right;
71 | this.innerHeight = this.outerHeight - this.margins.top - this.margins.bottom;
72 | this.reset();
73 | }
74 |
75 | public currentPerspective() {
76 | return this.useSentPerspective ? this.sent : this.received;
77 | };
78 |
79 | public reset() {
80 | this.sent = {
81 | xScale: null,
82 | yScale: null, // Used for packet_sent, packet_acked and packet_lost
83 | yCongestionScale: null, // Used for congestion window and bytes in flight
84 | xAxis: null,
85 | yAxis: null,
86 | yCongestionAxis: null,
87 | rangeX: [0, 0], // [minX, maxX]
88 | rangeY: [0, 0], // [minY, maxY]
89 | congestionRangeY: [0, 0], // [minY, maxY]
90 | originalRangeX: [0, 0], // [minX, maxX]
91 | originalRangeY: [0, 0], // [minY, maxY]
92 | originalCongestionRangeY: [0, 0], // [minY, maxY]
93 |
94 | drawScaleX: 1,
95 | drawScaleY: 1,
96 | };
97 | this.received = {
98 | xScale: null,
99 | yScale: null, // Used for packet_sent, packet_acked and packet_lost
100 | xAxis: null,
101 | yAxis: null,
102 | rangeX: [0, 0], // [minX, maxX]
103 | rangeY: [0, 0], // [minY, maxY]
104 | originalRangeX: [0, 0], // [minX, maxX]
105 | originalRangeY: [0, 0], // [minY, maxY]
106 |
107 | drawScaleX: 1,
108 | drawScaleY: 1,
109 | };
110 | this.metricUpdateLines = {
111 | bytes: new Array<[number, number]>(),
112 | cwnd: new Array<[number, number]>(),
113 | minRTT: new Array<[number, number]>(),
114 | smoothedRTT: new Array<[number, number]>(),
115 | lastRTT: new Array<[number, number]>(),
116 | };
117 | this.flowControlLines = {
118 | application: new Array<[number, number]>(),
119 | stream: new Array<[number, number]>(),
120 | }
121 | }
122 | };
123 |
124 | interface IPerspectiveInfo {
125 | xScale: ScaleLinear | null;
126 | yScale: ScaleLinear | null; // Used for packet_sent, packet_acked and packet_lost
127 | yCongestionScale?: ScaleLinear | null; // Used for congestion window and bytes in flight
128 | xAxis: Axis | null;
129 | yAxis: Axis | null;
130 | yCongestionAxis?:Axis | null;
131 | rangeX: [number, number]; // [minX, maxX]
132 | rangeY: [number, number]; // [minY, maxY]
133 | congestionRangeY?: [number, number]; // [minY, maxY]
134 | originalRangeX: [number, number]; // [minX, maxX]
135 | originalRangeY: [number, number], // [minY, maxY]
136 | originalCongestionRangeY?: [number, number], // [minY, maxY]
137 |
138 | drawScaleX: number,
139 | drawScaleY: number,
140 | }
141 |
--------------------------------------------------------------------------------
/visualizations/src/components/congestiongraph/renderer/RecoveryGraphState.ts:
--------------------------------------------------------------------------------
1 | import { MainGraphState } from './MainGraphState';
2 | import { Selection, Axis, ScaleLinear } from 'd3';
3 |
4 | export class RecoveryGraphState {
5 | // Fields
6 | public outerWidth = window.innerWidth;
7 | public outerHeight = 300;
8 | public margins = {
9 | top: 20,
10 | bottom: 60,
11 | left: 70,
12 | right: 70,
13 | };
14 | public innerWidth: number;
15 | public innerHeight: number;
16 |
17 | public graphSvg: Selection | null = null;
18 | public canvas: Selection | null = null;
19 | public canvasContext: CanvasRenderingContext2D | null = null;
20 |
21 | // Scales/Axii/Range data
22 | public xAxis: Axis | null = null; // xScale is shared with main chart
23 | public gxAxis: Selection | null = null; // Graphical element for the x axis
24 |
25 | public yScale: ScaleLinear | null = null;
26 | public yAxis: Axis | null = null;
27 | public gyAxis: Selection | null = null; // Graphical element for the y axis
28 | public originalRangeY: [number, number] = [0, 0]; // [minY, maxY]
29 | public rangeY: [number, number] = [0, 0]; // Current minY and maxY
30 |
31 | public zooming0RTTenabled:boolean = false;
32 |
33 |
34 | /* Methods */
35 | public constructor() {
36 | this.innerWidth = this.outerWidth - this.margins.left - this.margins.right;
37 | this.innerHeight = this.outerHeight - this.margins.top - this.margins.bottom;
38 | }
39 |
40 | public reset() {
41 | this.xAxis = null;
42 | this.yAxis = null;
43 | this.yScale = null;
44 | this.originalRangeY = [0, 0];
45 | this.rangeY = [0, 0];
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/visualizations/src/components/filemanager/netlogconverter/netlog.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as qlogschema from '@/data/QlogSchema';
3 |
4 | /* tslint:disable */
5 |
6 | export enum PacketType {
7 | initial = "ENCRYPTION_INITIAL",
8 | handshake = "ENCRYPTION_HANDSHAKE",
9 | zerortt = "ENCRYPTION_ZERO_RTT",
10 | onertt = "ENCRYPTION_FORWARD_SECURE",
11 | }
12 |
13 | export interface Netlog {
14 | constants: any,
15 | events: Array,
16 | }
17 |
18 | export interface Constants {
19 | logEventTypes: any,
20 | logSourceType: any,
21 | logEventPhase: any,
22 | timeTickOffset: number,
23 | }
24 |
25 | export interface Event {
26 | params: any,
27 | phase: number,
28 | source: EventSource,
29 | time: string,
30 | type: number,
31 | }
32 |
33 | export interface EventSource {
34 | id: number,
35 | start_time: string,
36 | type: number,
37 | }
38 |
39 | export interface QUIC_SESSION {
40 | cert_verify_flags?: number,
41 | connection_id?: string,
42 | host: string,
43 | network_isolation_key?: string,
44 | port?: number,
45 | privacy_mode?: string,
46 | require_confirmation?: boolean,
47 | versions?: string,
48 | }
49 |
50 | export interface QUIC_SESSION_PACKET_SENT {
51 | encryption_level: PacketType,
52 | packet_number: number,
53 | sent_time_us: number,
54 | size: number,
55 | transmission_type: string
56 | }
57 |
58 | export interface QUIC_SESSION_COALESCED_PACKET_SENT {
59 | info: string
60 | }
61 |
62 | export interface QUIC_SESSION_COALESCED_PACKET {
63 | total_length: string,
64 | padding_size: string,
65 | packets: string,
66 | }
67 |
68 | export interface QUIC_SESSION_TRANSPORT_PARAMETERS {
69 | quic_transport_parameters: string
70 | }
71 |
72 | export interface QUIC_SESSION_PADDING_FRAME {
73 | num_padding_bytes: number
74 | }
75 |
76 | export interface QUIC_SESSION_CRYPTO_FRAME {
77 | bytes?: string,
78 | data_length: number,
79 | encryption_level: string,
80 | offset: number
81 | }
82 |
83 | export interface QUIC_SESSION_ACK_FRAME {
84 | delta_time_largest_observed_us: number,
85 | largest_observed: number,
86 | smallest_observed: number, // see also: https://bugs.chromium.org/p/chromium/issues/detail?id=1112925
87 | missing_packets: Array,
88 | received_packet_times: Array
89 | }
90 |
91 | export interface QUIC_SESSION_RST_STREAM_FRAME {
92 | offset: number,
93 | quic_rst_stream_error: number,
94 | stream_id: number
95 | }
96 |
97 | export interface QUIC_SESSION_STOP_SENDING_FRAME {
98 | application_error_code: number,
99 | stream_id: number
100 | }
101 |
102 | export interface QUIC_SESSION_STREAM_FRAME {
103 | fin: boolean,
104 | length: number,
105 | offset: number,
106 | stream_id: number
107 | }
108 |
109 | export interface QUIC_SESSION_WINDOW_UPDATE_FRAME {
110 | byte_offset: number,
111 | stream_id: number
112 | }
113 |
114 | export interface QUIC_SESSION_CRYPTO_HANDSHAKE_MESSAGE {
115 | quic_crypto_handshake_message: string,
116 | }
117 |
118 | export interface QUIC_SESSION_CONNECTION_CLOSE_FRAME_SENT {
119 | details: string,
120 | quic_error: number
121 | }
122 |
123 | export interface QUIC_SESSION_PACKET_RECEIVED {
124 | peer_address: string,
125 | self_address: string,
126 | size: number
127 | }
128 |
129 | export interface QUIC_SESSION_PACKET_LOST {
130 | detection_time_us: number,
131 | packet_number: number,
132 | transmission_type: string,
133 | }
134 |
135 | export enum LONG_HEADER_TYPE {
136 | initial = "INITIAL",
137 | handshake = "HANDSHAKE",
138 | zerortt = "ZERO_RTT_PROTECTED",
139 | version_negotiation = "VERSION_NEGOTIATION",
140 | retry = "RETRY",
141 | invalid = "INVALID_PACKET_TYPE",
142 | }
143 |
144 | export interface QUIC_SESSION_UNAUTHENTICATED_PACKET_HEADER_RECEIVED {
145 | connection_id: string,
146 | header_format: string,
147 | long_header_type?: LONG_HEADER_TYPE,
148 | packet_number: number
149 | }
150 |
151 | export interface QUIC_SESSION_DROPPED_UNDECRYPTABLE_PACKET {
152 | encryption_level: string
153 | }
154 |
155 | export interface QUIC_SESSION_CLOSED {
156 | details: string,
157 | from_peer: boolean,
158 | quic_error: number
159 | }
160 |
161 | export interface HTTP3_STREAM_CREATED {
162 | stream_id: number,
163 | }
164 |
165 | export interface HTTP3_MAX_PUSH_ID {
166 | push_id: number,
167 | }
168 |
169 | export interface HTTP3_PRIORITY_UPDATE {
170 | prioritized_element_id: number,
171 | priority_field_value: string,
172 | type: string
173 | }
174 |
175 | export interface HTTP3_DATA_FRAME {
176 | payload_length: number,
177 | stream_id: number
178 | }
179 |
180 | export interface HTTP3_UNKNOWN_FRAME {
181 | frame_type: number,
182 | payload_length: number,
183 | stream_id: number
184 | }
185 |
186 | export interface HTTP3_HEADERS {
187 | headers: Map,
188 | stream_id: number,
189 | }
190 |
191 | export interface HTTP3_SETTINGS {
192 | SETTINGS_MAX_HEADER_LIST_SIZE: number,
193 | SETTINGS_QPACK_BLOCKED_STREAMS: number,
194 | SETTINGS_QPACK_MAX_TABLE_CAPACITY: number,
195 | }
196 |
--------------------------------------------------------------------------------
/visualizations/src/components/filemanager/newlineconverter/newlinejsontoqlog.ts:
--------------------------------------------------------------------------------
1 | import * as qlogschema from '@/data/QlogSchema';
2 |
3 | export default class NewlineJSONToQlog {
4 |
5 | public static async convert( inputStream:ReadableStream ) : Promise {
6 |
7 | console.log("NewlineJSONToQlog: converting newline delimited JSON file");
8 |
9 | // make proper qlogschema.IQLog again when we've updated the schema to match draft-02 proper
10 | const qlogFile:any = { qlog_version: "draft-02", qlog_format: qlogschema.LogFormat.NDJSON, traces: new Array() } as qlogschema.IQLog;
11 |
12 |
13 | const rawJSONentries = await NewlineJSONToQlog.parseNDJSON( inputStream );
14 |
15 | if ( rawJSONentries.length === 0 ) {
16 | console.error("NewlineJSONToQlog: no entries found in the loaded file...");
17 |
18 | return qlogFile;
19 | }
20 |
21 | // in NDJSON format, we should first have the file "header", a separate object containing the qlog metadata
22 | // and then we should have a single entry per event after that.
23 |
24 | const header = rawJSONentries.shift();
25 |
26 | if ( header.qlog_version === undefined || header.qlog_format !== qlogschema.LogFormat.NDJSON || header.trace === undefined ) {
27 | console.error("NewlineJSONToQlog: File did not start with the proper qlog header! Aborting...", header);
28 |
29 | return qlogFile;
30 | }
31 |
32 | // copy over everything, but we'll handle trace separately below
33 | for ( const key of Object.keys(header) ) {
34 | if ( key !== "trace" ) {
35 | (qlogFile as any)[key] = header[key];
36 | }
37 | }
38 |
39 | // NDJSON files have just a single trace by definition
40 | const trace:qlogschema.ITrace = {
41 | vantage_point: {
42 | type: qlogschema.VantagePointType.unknown,
43 | },
44 | events: [],
45 | };
46 |
47 | // copy over everything
48 | for ( const key of Object.keys(header.trace) ) {
49 | (trace as any)[ key ] = header.trace[key];
50 | }
51 |
52 | trace.events = rawJSONentries; // the header was removed by calling shift() above, so these should be the raw events
53 |
54 |
55 | qlogFile.traces = [ trace ];
56 |
57 | return qlogFile as qlogschema.IQLog;
58 | }
59 |
60 | protected static async parseNDJSON( inputStream:ReadableStream ) : Promise> {
61 |
62 | let resolver:any = undefined;
63 | let rejecter:any = undefined;
64 |
65 | const output = new Promise>( (resolve, reject) => {
66 | resolver = resolve;
67 | rejecter = reject;
68 | });
69 |
70 | const entries:Array = [];
71 |
72 | // const jsonStream = ndjsonStream( inputStream );
73 | const jsonStream = NewlineJSONToQlog.createNewlineTransformer( inputStream );
74 |
75 | const streamReader = jsonStream.getReader();
76 | let read:any = undefined;
77 |
78 | streamReader.read().then( read = ( result:any ) => {
79 |
80 | // at the end of the stream, this function is called one last time
81 | // with result.done set and an empty result.value
82 | if ( result.done ) {
83 | resolver( entries );
84 |
85 | return;
86 | }
87 |
88 | // use destructuring instead of concat to merge the objects,
89 | // see https://dev.to/uilicious/javascript-array-push-is-945x-faster-than-array-concat-1oki
90 | entries.push( ...result.value );
91 |
92 | // console.log("parseNDJSON: DEBUG : ", result.value.length, result.value );
93 |
94 | streamReader.read().then( read );
95 | } );
96 |
97 | return output;
98 | }
99 |
100 | // this code was taken largely from the can-ndjson-stream project (https://www.npmjs.com/package/can-ndjson-stream)
101 | // that project however surfaces each object individually, which incurs quite a large message passing overhead from the transforming stream
102 | // to the reading stream.
103 | // Our custom version here instead batches all read objects from a single chunk and propagates those up in 1 time, which is much faster for our use case.
104 |
105 | // copyright notice for this function:
106 | /*
107 | The MIT License (MIT)
108 |
109 | Copyright 2017 Justin Meyer (justinbmeyer@gmail.com), Fang Lu
110 | (cc2lufang@gmail.com), Siyao Wu (wusiyao@umich.edu), Shang Jiang
111 | (mrjiangshang@gmail.com)
112 |
113 | Permission is hereby granted, free of charge, to any person obtaining a copy
114 | of this software and associated documentation files (the "Software"), to deal
115 | in the Software without restriction, including without limitation the rights
116 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
117 | copies of the Software, and to permit persons to whom the Software is
118 | furnished to do so, subject to the following conditions:
119 |
120 | The above copyright notice and this permission notice shall be included in all
121 | copies or substantial portions of the Software.
122 |
123 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
124 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
125 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
126 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
127 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
128 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
129 | SOFTWARE.
130 | */
131 | protected static createNewlineTransformer( inputStream:ReadableStream ):ReadableStream {
132 |
133 | let is_reader:ReadableStreamReader|undefined = undefined;
134 | let cancellationRequest:boolean = false;
135 |
136 | let readLineCount = 0;
137 |
138 | return new ReadableStream({
139 | start: (controller) => {
140 | const reader = inputStream.getReader();
141 | is_reader = reader;
142 |
143 | const decoder = new TextDecoder();
144 | let data_buf = "";
145 |
146 | reader.read().then(function processResult(result:any):any {
147 |
148 | // console.log("parseNDJSON:parse ", result);
149 |
150 | // at the end of the stream, this function is called one last time
151 | // with result.done set and an empty result.value
152 | if (result.done) {
153 | if (cancellationRequest) {
154 | // Immediately exit
155 | return;
156 | }
157 |
158 | // try to process the last part of the file if possible
159 | data_buf = data_buf.trim();
160 | if (data_buf.length !== 0) {
161 | ++readLineCount;
162 |
163 | try {
164 | const data_l = JSON.parse(data_buf);
165 | controller.enqueue( [data_l] ); // need to wrap in array, since that's what calling code expects
166 | }
167 | catch (e) {
168 | console.error("NewlineJSONToQlog: line #" + readLineCount + " was invalid JSON. Skipping and continuing.", data_buf);
169 | // // TODO: what does this do practically? We probably want to (silently?) ignore errors?
170 | // controller.error(e);
171 | // return;
172 | }
173 | }
174 |
175 | controller.close();
176 |
177 | return;
178 | }
179 |
180 | const data = decoder.decode(result.value, {stream: true});
181 | data_buf += data;
182 |
183 | const lines = data_buf.split("\n");
184 |
185 | const output = []; // batch results together to reduce message passing overhead
186 |
187 | for ( let i = 0; i < lines.length - 1; ++i) {
188 |
189 | const l = lines[i].trim();
190 |
191 | if (l.length > 0) {
192 | ++readLineCount;
193 |
194 | try {
195 | const data_line = JSON.parse(l);
196 | // controller.enqueue(data_line) would immediately pass the single read object on, but we batch it instead on the next line
197 | output.push( data_line );
198 | }
199 | catch (e) {
200 | console.error("NewlineJSONToQlog: line #" + readLineCount + " was invalid JSON. Skipping and continuing.", l);
201 |
202 | // // TODO: what does this do practically? We probably want to (silently?) ignore errors?
203 | // controller.error(e);
204 | // cancellationRequest = true;
205 | // reader.cancel();
206 |
207 | // return;
208 | }
209 | }
210 | }
211 | data_buf = lines[lines.length - 1];
212 |
213 | controller.enqueue( output );
214 |
215 | return reader.read().then(processResult);
216 | });
217 |
218 | },
219 |
220 | cancel: (reason) => {
221 | console.warn("NewlineJSONToQlog:parseNDJSON : Cancel registered due to ", reason);
222 |
223 | cancellationRequest = true;
224 |
225 | if ( is_reader !== undefined ) {
226 | is_reader.cancel();
227 | }
228 | },
229 | },
230 | // TODO: we tried to optimize a bit with this, but it doesn't seem to work (printing chunks above gives chunks of 65K, not 260K)
231 | // didn't immediately find a good solution for this though, seems like chunk-sizing APIs aren't well supported yet in browsers
232 | {
233 | highWaterMark: 4, // read up to 1 chunk of the following size
234 | size: (chunk) => { return 262144; },
235 | });
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/visualizations/src/components/filemanager/newlineconverter/textsequencejsontoqlog.ts:
--------------------------------------------------------------------------------
1 | import * as qlogschema from '@/data/QlogSchema';
2 |
3 | export default class TextSequenceJSONToQlog {
4 |
5 | public static async convert( inputStream:ReadableStream ) : Promise {
6 |
7 | console.log("TextSequenceJSONToQlog: converting textsequence JSON file");
8 |
9 | // make proper qlogschema.IQLog again when we've updated the schema to match draft-02 proper
10 | const qlogFile:any = { qlog_version: "draft-02", qlog_format: qlogschema.LogFormat.JSONSEQ, traces: new Array() } as qlogschema.IQLog;
11 |
12 |
13 | const rawJSONentries = await TextSequenceJSONToQlog.parseTextSequences( inputStream );
14 |
15 | if ( rawJSONentries.length === 0 ) {
16 | console.error("TextSequenceJSONToQlog: no entries found in the loaded file...");
17 |
18 | return qlogFile;
19 | }
20 |
21 | // in json-seq format, we should first have the file "header", a separate object containing the qlog metadata
22 | // and then we should have a single entry per event after that.
23 |
24 | const header = rawJSONentries.shift();
25 |
26 | if ( header.qlog_version === undefined || header.qlog_format !== qlogschema.LogFormat.JSONSEQ || header.trace === undefined ) {
27 | console.error("TextSequenceJSONToQlog: File did not start with the proper qlog header (needs version, format and trace)! Aborting...", header);
28 |
29 | return undefined;
30 | }
31 |
32 | // copy over everything, but we'll handle trace separately below
33 | for ( const key of Object.keys(header) ) {
34 | if ( key !== "trace" ) {
35 | (qlogFile as any)[key] = header[key];
36 | }
37 | }
38 |
39 | // json-seq files have just a single trace by definition
40 | const trace:qlogschema.ITrace = {
41 | vantage_point: {
42 | type: qlogschema.VantagePointType.unknown,
43 | },
44 | events: [],
45 | };
46 |
47 | // copy over everything
48 | for ( const key of Object.keys(header.trace) ) {
49 | (trace as any)[ key ] = header.trace[key];
50 | }
51 |
52 | trace.events = rawJSONentries; // the header was removed by calling shift() above, so these should be the raw events
53 |
54 |
55 | qlogFile.traces = [ trace ];
56 |
57 | return qlogFile as qlogschema.IQLog;
58 | }
59 |
60 | protected static async parseTextSequences( inputStream:ReadableStream ) : Promise> {
61 |
62 | let resolver:any = undefined;
63 | let rejecter:any = undefined;
64 |
65 | const output = new Promise>( (resolve, reject) => {
66 | resolver = resolve;
67 | rejecter = reject;
68 | });
69 |
70 | const entries:Array = [];
71 |
72 | const jsonStream = TextSequenceJSONToQlog.createRecordTransformer( inputStream );
73 |
74 | const streamReader = jsonStream.getReader();
75 | let read:any = undefined;
76 |
77 | streamReader.read().then( read = ( result:any ) => {
78 |
79 | // at the end of the stream, this function is called one last time
80 | // with result.done set and an empty result.value
81 | if ( result.done ) {
82 | resolver( entries );
83 |
84 | return;
85 | }
86 |
87 | // use destructuring instead of concat to merge the objects,
88 | // see https://dev.to/uilicious/javascript-array-push-is-945x-faster-than-array-concat-1oki
89 | entries.push( ...result.value );
90 |
91 | // console.log("parseNDJSON: DEBUG : ", result.value.length, result.value );
92 |
93 | streamReader.read().then( read );
94 | } );
95 |
96 | return output;
97 | }
98 |
99 | // this code was taken largely from the can-ndjson-stream project (https://www.npmjs.com/package/can-ndjson-stream)
100 | // that project however surfaces each object individually, which incurs quite a large message passing overhead from the transforming stream
101 | // to the reading stream.
102 | // Our custom version here instead batches all read objects from a single chunk and propagates those up in 1 time, which is much faster for our use case.
103 | // it also replaces splitting on \n by splitting on the RecordSeparator character for json-seq. Everything else is the same as NDJSON handling.
104 |
105 | // copyright notice for this function:
106 | /*
107 | The MIT License (MIT)
108 |
109 | Copyright 2017 Justin Meyer (justinbmeyer@gmail.com), Fang Lu
110 | (cc2lufang@gmail.com), Siyao Wu (wusiyao@umich.edu), Shang Jiang
111 | (mrjiangshang@gmail.com)
112 |
113 | Permission is hereby granted, free of charge, to any person obtaining a copy
114 | of this software and associated documentation files (the "Software"), to deal
115 | in the Software without restriction, including without limitation the rights
116 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
117 | copies of the Software, and to permit persons to whom the Software is
118 | furnished to do so, subject to the following conditions:
119 |
120 | The above copyright notice and this permission notice shall be included in all
121 | copies or substantial portions of the Software.
122 |
123 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
124 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
125 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
126 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
127 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
128 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
129 | SOFTWARE.
130 | */
131 | protected static createRecordTransformer( inputStream:ReadableStream ):ReadableStream {
132 |
133 | let is_reader:ReadableStreamReader|undefined = undefined;
134 | let cancellationRequest:boolean = false;
135 |
136 | let readRecordCount = 0;
137 |
138 | return new ReadableStream({
139 | start: (controller) => {
140 | const reader = inputStream.getReader();
141 | is_reader = reader;
142 |
143 | const decoder = new TextDecoder();
144 | let data_buf = "";
145 |
146 | reader.read().then(function processResult(result:any):any {
147 |
148 | // at the end of the stream, this function is called one last time
149 | // with result.done set and an empty result.value
150 | if (result.done) {
151 | if (cancellationRequest) {
152 | // Immediately exit
153 | return;
154 | }
155 |
156 | // try to process the last part of the file if possible
157 | data_buf = data_buf.trim();
158 | if (data_buf.length !== 0) {
159 | ++readRecordCount;
160 |
161 | try {
162 | const data_l = JSON.parse(data_buf);
163 | controller.enqueue( [data_l] ); // need to wrap in array, since that's what calling code expects
164 | }
165 | catch (e) {
166 | console.error("TextSequenceJSONToQlog:ondone record #" + readRecordCount + " was invalid JSON. Skipping and continuing.", data_buf);
167 | // // TODO: what does this do practically? We probably want to (silently?) ignore errors?
168 | // controller.error(e);
169 | // return;
170 | }
171 | }
172 |
173 | controller.close();
174 |
175 | return;
176 | }
177 |
178 | const data = decoder.decode(result.value, {stream: true});
179 | data_buf += data;
180 |
181 | const records = data_buf.split("\u001E"); // \u001E is the Record Separator character
182 |
183 | const output = []; // batch results together to reduce message passing overhead
184 |
185 | console.log("TextsequenceJSONToQlog:createRecordTransformer Amount of records", records.length);
186 |
187 | for ( let i = 0; i < records.length - 1; ++i) {
188 |
189 | const r = records[i].trim();
190 |
191 | if (r.length > 0) {
192 | ++readRecordCount;
193 |
194 | try {
195 | const data_record = JSON.parse(r);
196 | // controller.enqueue(data_record) would immediately pass the single read object on, but we batch it instead on the next line
197 | output.push( data_record );
198 | }
199 | catch (e) {
200 | console.error("TextSequenceJSONToQlog: line #" + readRecordCount + " was invalid JSON. Skipping and continuing.", r, records.length);
201 | return;
202 |
203 | // // TODO: what does this do practically? We probably want to (silently?) ignore errors?
204 | // controller.error(e);
205 | // cancellationRequest = true;
206 | // reader.cancel();
207 |
208 | // return;
209 | }
210 | }
211 | }
212 | data_buf = records[records.length - 1];
213 |
214 | controller.enqueue( output );
215 |
216 | return reader.read().then(processResult);
217 | });
218 |
219 | },
220 |
221 | cancel: (reason) => {
222 | console.warn("TextSequenceJSONToQlog:parseTextSequences : Cancel registered due to ", reason);
223 |
224 | cancellationRequest = true;
225 |
226 | if ( is_reader !== undefined ) {
227 | is_reader.cancel();
228 | }
229 | },
230 | },
231 | // TODO: we tried to optimize a bit with this, but it doesn't seem to work (printing chunks above gives chunks of 65K, not 260K)
232 | // didn't immediately find a good solution for this though, seems like chunk-sizing APIs aren't well supported yet in browsers
233 | {
234 | highWaterMark: 4, // read up to 1 chunk of the following size
235 | size: (chunk) => { return 262144; },
236 | });
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/visualizations/src/components/filemanager/pcapconverter/qlog_tcp_tls_h2.ts:
--------------------------------------------------------------------------------
1 | import * as qlogschema from "@/data/QlogSchema";
2 |
3 | export enum EventCategory {
4 | tcp = "transport",
5 | tls = "tls",
6 | http2 = "http2",
7 | }
8 |
9 | export enum TCPEventType {
10 | packet_sent = "packet_sent",
11 | packet_received = "packet_received",
12 | }
13 |
14 | export enum TLSEventType {
15 | record_created = "record_created",
16 | record_parsed = "record_parsed",
17 | }
18 |
19 | export type quint64 = number | string;
20 | export type qbytes = string;
21 |
22 | export interface IRawInfo {
23 | length?: quint64,
24 | payload_length?: quint64,
25 |
26 | data?: qbytes
27 | }
28 |
29 | export enum HTTP2EventType {
30 | frame_created = "frame_created",
31 | frame_parsed = "frame_parsed",
32 | }
33 |
34 | export interface IEventPacketSent {
35 | header: ITCPPacketHeader,
36 | raw?: IRawInfo,
37 |
38 | details?: Array,
39 | }
40 |
41 | export interface IEventPacketReceived{
42 | header: ITCPPacketHeader,
43 | raw?: IRawInfo,
44 |
45 | details?: Array,
46 |
47 | // TODO: add : options?; Array,
48 | }
49 |
50 | export interface ITCPPacketHeader {
51 | sequence_number: number;
52 | packet_size?: number;
53 | payload_length?: number;
54 | header_length?:number;
55 |
56 | // QUIC qlogschema compatibility, not actually used here
57 | packet_type: qlogschema.PacketType; // TCP can be considered as always QUIC's 1RTT equivalent. Need to keep parity with QUIC-based qlog atm
58 | packet_number: qlogschema.quint64;
59 | }
60 |
61 | export type IPacketDetail = IPacketAcks | IPacketFlowControl;
62 |
63 | export enum TCPPacketDetailName {
64 | ack = "ack",
65 | flow_control = "flow_control",
66 | flags = "flags",
67 | }
68 |
69 | export interface IPacketFlags {
70 | type:TCPPacketDetailName.flags,
71 |
72 | syn?:boolean,
73 | ack?:boolean,
74 | reset?:boolean,
75 | fin?:boolean,
76 | }
77 |
78 | export interface IPacketAcks {
79 | type:TCPPacketDetailName.ack,
80 | }
81 |
82 | export interface IPacketFlowControl {
83 | type:TCPPacketDetailName.flow_control,
84 | }
85 |
86 | export interface IEventRecordCreated {
87 | header:IRecordHeader,
88 |
89 | raw?:string
90 | }
91 |
92 | export interface IEventRecordParsed {
93 | header:IRecordHeader,
94 |
95 | raw?:string
96 | }
97 |
98 | export interface IRecordHeader {
99 | content_type?:"handshake"|"alert"|"application"|"change-cipherspec"|"unknown",
100 | version?:string,
101 | payload_length?:number,
102 | header_length?:number,
103 | trailer_length?:number,
104 |
105 | DEBUG_wiresharkFrameNumber?:number,
106 |
107 | // QUIC qlogschema compatibility, not actually used here
108 | packet_type:qlogschema.PacketType, // QUIC qlog compat mode
109 | packet_number:quint64, // QUIC qlog compat mode
110 | }
111 |
112 |
113 | export interface IEventH2FrameCreated {
114 | stream_id:number,
115 | frame:HTTP2Frame,
116 | payload_length?:number,
117 | header_length?:number,
118 |
119 | raw?:string
120 | }
121 |
122 | export interface IEventH2FrameParsed {
123 | stream_id:number,
124 | frame:HTTP2Frame,
125 | payload_length?:number,
126 | header_length?:number,
127 |
128 | raw?:string
129 | }
130 |
131 | export enum HTTP2FrameTypeName {
132 | data = "data",
133 | headers = "headers",
134 | priority = "priority",
135 | reset_stream = "reset_stream",
136 | settings = "settings",
137 | push_promise = "push_promise",
138 | ping = "ping",
139 | go_away = "go_away",
140 | window_update = "window_update",
141 | continuation = "continuation",
142 | unknown = "unknown",
143 | magic = "magic",
144 | }
145 |
146 |
147 | export type HTTP2Frame = IDataFrame | IHeadersFrame | ISettingsFrame | IUnknownFrame | IAnyFrame;
148 |
149 | export interface IDataFrame{
150 | frame_type:HTTP2FrameTypeName.data,
151 |
152 | byte_length?:number,
153 | stream_end?:boolean,
154 |
155 | raw?:string
156 | }
157 |
158 | export interface IHeadersFrame {
159 | frame_type:HTTP2FrameTypeName.headers,
160 | byte_length?:number,
161 |
162 | headers:Array,
163 |
164 | stream_end?:boolean,
165 | raw?:string
166 | }
167 |
168 | export interface IHTTPHeader {
169 | name:string,
170 | value:string,
171 | }
172 |
173 | export interface ISettingsFrame {
174 | frame_type:HTTP2FrameTypeName.settings,
175 | byte_length?:number,
176 |
177 | raw?:string
178 | }
179 |
180 | // TODO: replace with proper frame definitions for all the different frame types!
181 | export interface IAnyFrame {
182 | frame_type:HTTP2FrameTypeName,
183 | byte_length?:number,
184 |
185 | raw?:string
186 | }
187 |
188 | export interface IUnknownFrame {
189 | frame_type:HTTP2FrameTypeName.unknown,
190 | byte_length?:number,
191 |
192 | raw?:string
193 | }
194 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/MultiplexingGraphCollapsedRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{(connection !== undefined) ? connection.parent.filename + " : " + connection.getLongName() : ""}}
6 |
7 |
8 |
9 |
10 | Selected stream's details
11 |
12 |
13 |
14 |
Stream
{{ streamDetail.stream_id }} : Requested at {{ streamDetail.data.requestTime.toFixed(2) }}ms. Transmitted from {{ streamDetail.data.startTime.toFixed(2) }}ms to {{streamDetail.data.endTime.toFixed(2)}}ms ({{ (streamDetail.data.endTime.toFixed(2) - streamDetail.data.startTime.toFixed(2)).toFixed(2) }}ms). {{streamDetail.data.totalData}} bytes spread over {{streamDetail.data.frameCount}} frames (including retransmits).
15 |
16 |
17 | HTTP/3 HEADERS seen at {{ streamDetail.data.h3Info.headersTime.toFixed(2) }}ms. HTTP/3 PRIORITY Update seen at {{ streamDetail.data.h3Info.priorityUpdateTime.toFixed(2) }}ms. Priority info (if any): {{ streamDetail.data.h3Info.priorityString }}
18 |
19 |
20 |
21 |
22 |
23 |
24 | Waterfall
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Simulated FIFO order
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Multiplexed data flow
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Byterange per STREAM frame
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
196 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/MultiplexingGraphConfigurator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Select a trace via the dropdown(s) below to visualize it in the stream graph
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 | Show waterfall
20 |
21 |
22 |
27 | Show byte ranges
28 |
29 |
30 | Load all connections at once
31 |
32 |
33 | Please load a trace file to visualize it
34 | Loading files...
35 |
36 |
37 |
38 |
39 |
40 |
41 |
50 |
51 |
128 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/MultiplexingGraphContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
33 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/MultiplexingGraphRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
100 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/data/MultiplexingGraphConfig.ts:
--------------------------------------------------------------------------------
1 | import Connection from "@/data/Connection";
2 |
3 | export default class MultiplexingGraphConfig {
4 |
5 | public collapsed:boolean = true;
6 | public showwaterfall:boolean = true;
7 | public showbyteranges:boolean = true;
8 | public connections:Array = new Array();
9 | }
10 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/renderer/MultiplexingGraphD3ByterangesRenderer.ts:
--------------------------------------------------------------------------------
1 | import QlogConnection from '@/data/Connection';
2 | import * as d3 from 'd3';
3 | import * as qlog from '@/data/QlogSchema';
4 | import StreamGraphDataHelper from './MultiplexingGraphDataHelper';
5 |
6 |
7 | export default class MultiplexingGraphD3ByterangesRenderer {
8 |
9 | public containerID:string;
10 |
11 | public rendering:boolean = false;
12 | protected svg!:any;
13 | protected connection!:QlogConnection;
14 |
15 | protected height = 500;
16 |
17 | private dimensions:any = {};
18 |
19 | private updateZoom:any;
20 |
21 | constructor(containerID:string) {
22 | this.containerID = containerID;
23 | }
24 |
25 | public async render( allFrames:Array, allDataMoved:Array, streamID:number ):Promise {
26 | if ( this.rendering ) {
27 | return false;
28 | }
29 |
30 | console.log("MultiplexingGraphD3ByterangesRenderer:render", streamID);
31 |
32 | this.rendering = true;
33 |
34 | const canContinue:boolean = this.setup();
35 |
36 | if ( !canContinue ) {
37 | this.rendering = false;
38 |
39 | return false;
40 | }
41 |
42 | await this.renderLive( allFrames, allDataMoved, streamID );
43 | this.rendering = false;
44 |
45 | return true;
46 | }
47 |
48 | public zoom(newXDomain:any) {
49 | this.updateZoom( newXDomain );
50 | }
51 |
52 | protected setup() {
53 |
54 | const container:HTMLElement = document.getElementById(this.containerID)!;
55 |
56 | this.dimensions.margin = {top: 20, right: 40, bottom: 20, left: 20};
57 |
58 | // width and height are the INTERNAL widths (so without the margins)
59 | this.dimensions.width = container.clientWidth - this.dimensions.margin.left - this.dimensions.margin.right;
60 | this.dimensions.height = this.height;
61 |
62 |
63 | // clear old rendering
64 | d3.select( "#" + this.containerID ).selectAll("*").remove();
65 |
66 | this.svg = d3.select("#" + this.containerID)
67 | .append("svg")
68 | .attr("width", this.dimensions.width + this.dimensions.margin.left + this.dimensions.margin.right)
69 | .attr("height", this.dimensions.height + this.dimensions.margin.top + this.dimensions.margin.bottom)
70 | // .attr("viewBox", [0, 0, this.dimensions.width, this.dimensions.height])
71 | .attr("xmlns", "http://www.w3.org/2000/svg")
72 | .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
73 | .attr("font-family", "Trebuchet-ms")
74 | .append("g")
75 | .attr("transform",
76 | "translate(" + this.dimensions.margin.left + "," + this.dimensions.margin.top + ")");
77 |
78 |
79 | return true;
80 | }
81 |
82 | protected async renderLive( allFrames:Array, allDataMoved:Array, streamID:number ) {
83 | console.log("Rendering multiplexing byteranges graph", streamID);
84 |
85 | const streamFrames = allFrames.filter( (d:any) => "" + d.streamID === "" + streamID );
86 | const dataMoved = allDataMoved.filter( (d:any) => "" + d.streamID === "" + streamID );
87 |
88 | // final frame isn't necessarily the last one in the file, due to retransmits
89 | // so we need to actually search for the largest one
90 | let maxBytes = 0;
91 | for ( const frame of streamFrames ) {
92 | const max = frame.offset + frame.length - 1;
93 | if ( max > maxBytes ){
94 | maxBytes = max;
95 | }
96 | }
97 |
98 | const xDomain = d3.scaleLinear()
99 | .domain([1, allFrames[ allFrames.length - 1 ].countEnd ])
100 | .range([ 0, this.dimensions.width ]);
101 |
102 |
103 | const xAxis = this.svg.append("g");
104 | xAxis.call(d3.axisTop(xDomain));
105 |
106 | const yDomain = d3.scaleLinear()
107 | .domain([0, maxBytes ])
108 | // .domain([0, 200000 ])
109 | // .range([this.dimensions.height, 0]);
110 | .range([0, this.dimensions.height]);
111 |
112 | const yAxis = this.svg.append("g")
113 | .attr("transform", "translate(0, 0)");
114 |
115 | yAxis.call(d3.axisRight(yDomain));
116 |
117 | const clip = this.svg.append("defs").append("SVG:clipPath")
118 | .attr("id", "byterange-clip")
119 | .append("SVG:rect")
120 | .attr("width", this.dimensions.width )
121 | .attr("height", this.dimensions.height )
122 | .attr("x", 0 )
123 | .attr("y", 0);
124 |
125 |
126 | const packetSidePadding = 0.3;
127 |
128 | const rects = this.svg.append('g')
129 | .attr("clip-path", "url(#byterange-clip)");
130 |
131 | const widthModifier = 1;
132 | const heightModifier = 1;
133 |
134 | rects
135 | .selectAll("rect.packet")
136 | .data( streamFrames )
137 | .enter()
138 | .append("rect")
139 | .attr("x", (d:any) => { return xDomain(d.countStart); } )
140 | .attr("y", (d:any) => yDomain(d.offset) )
141 | .attr("fill", (d:any) => StreamGraphDataHelper.StreamIDToColor("" + d.streamID)[0] )
142 | .style("opacity", 1)
143 | .attr("class", "packet")
144 | .attr("width", (d:any) => Math.max(1, xDomain(d.countEnd) - xDomain(d.countStart)) * widthModifier)
145 | .attr("height", (d:any) => Math.max(1, yDomain( d.length - 1)) * heightModifier)
146 |
147 | const opacity = 0.2;
148 | rects
149 | .selectAll("rect.pingback")
150 | .data( streamFrames )
151 | .enter()
152 | .append("rect")
153 | .attr("x", (d:any) => xDomain(1) )
154 | .attr("y", (d:any) => yDomain(d.offset) )
155 | .attr("fill", (d:any) => StreamGraphDataHelper.StreamIDToColor("" + d.streamID)[0] )
156 | .style("opacity", opacity)
157 | .attr("class", "pingback")
158 | .attr("width", (d:any) => Math.max(1, xDomain(d.countEnd) - xDomain(1)))
159 | .attr("height", (d:any) => yDomain( d.length - 1))
160 | // .style("pointer-events", "all")
161 |
162 | const movedOffset = 1;
163 | rects
164 | .selectAll("rect.dataMoved")
165 | .data( dataMoved )
166 | .enter()
167 | .append("rect")
168 | .attr("x", (d:any) => xDomain(d.countStart + movedOffset) )
169 | .attr("y", (d:any) => yDomain(d.offset) )
170 | .attr("fill", "black" )
171 | .style("opacity", 1)
172 | .attr("class", "dataMoved")
173 | .attr("width", (d:any) => Math.max(1, xDomain(d.countEnd) - xDomain(d.countStart)) * widthModifier)
174 | .attr("height", (d:any) => yDomain( d.offset + d.length - 1) - yDomain(d.offset));
175 |
176 | this.updateZoom = (newXDomain:any) => {
177 |
178 | // recover the new scale
179 | const newX = newXDomain; // d3.event.transform.rescaleX(xDomain);
180 |
181 | // nicely stays within indexes!
182 | const startIndex = Math.max(0, Math.ceil(newX.domain()[0]) + 1);
183 | const endIndex = Math.min( allFrames.length - 1, Math.floor(newX.domain()[1]) - 1);
184 |
185 | if ( endIndex > allFrames.length ) {
186 | console.error("Something went wrong transforming Y domain byterangeszoom", endIndex, allFrames.length );
187 | }
188 |
189 | // console.log("Looking for Y values between", startIndex, endIndex);
190 |
191 | let startY:number = Number.MAX_VALUE;
192 | let endY:number = 0;
193 |
194 | // TODO: first frame we find doesn't necessarily have the lowest offset... need to loop through the entire range and find lowest offset... auch
195 |
196 | for ( let i = startIndex; i < endIndex; ++i ){
197 | const frame = allFrames[i];
198 | if ( "" + frame.streamID === "" + streamID ) {
199 | // console.log("Found frame for stream at index ",i, " with offset ", frame.offset, frame );
200 | if ( frame.offset < startY ) {
201 | startY = parseInt(frame.offset, 0);
202 | }
203 | }
204 | }
205 |
206 | for ( let i = endIndex; i > startIndex; --i ) {
207 | const frame = allFrames[i];
208 | if ( !frame ){
209 | // console.error("Frame at index ", i, "Couldn't be found... weird");
210 | continue;
211 | }
212 |
213 | if ( "" + frame.streamID === "" + streamID ) {
214 | if ( parseInt(frame.offset, 10) + parseInt(frame.length, 10) - 1 > endY ) {
215 | endY = parseInt(frame.offset, 0) + parseInt(frame.length, 10) - 1;
216 | }
217 | }
218 | }
219 |
220 | // console.log("Y new domain", startY, endY);
221 | if ( startY > endY ) {
222 | console.error("Something went terribly wrong", startY, endY);
223 | }
224 |
225 | const newY = yDomain.copy().domain( [startY, endY] );
226 |
227 |
228 | xAxis.call(d3.axisTop(newX));
229 | yAxis.call(d3.axisLeft(newY));
230 |
231 | // update position
232 | rects
233 | .selectAll(".packet")
234 | .attr("x", (d:any) => newX(d.countStart) )
235 | .attr("width", (d:any) => Math.max(1, newX(d.countEnd) - newX(d.countStart)))
236 | .attr("y", (d:any) => newY(d.offset) )
237 | .attr("height", (d:any) => Math.max(1, newY( d.offset + d.length - 1) - newY( d.offset )) )
238 |
239 |
240 | rects
241 | .selectAll(".pingback")
242 | .attr("x", (d:any) => newX(1) )
243 | .attr("width", (d:any) => newX(d.countEnd) - newX(1) )
244 | .attr("y", (d:any) => newY(d.offset) )
245 | .attr("height", (d:any) => { return newY( d.offset + d.length - 1) - newY(d.offset);} )
246 |
247 |
248 | rects
249 | .selectAll(".dataMoved")
250 | .attr("x", (d:any) => newX(d.countStart + movedOffset) )
251 | .attr("width", (d:any) => Math.max(1, newX(d.countEnd) - newX(d.countStart)) )
252 | .attr("y", (d:any) => newY( d.offset ) )
253 | .attr("height", (d:any) => { return newY( d.offset + d.length - 1) - newY(d.offset); } )
254 | };
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/renderer/MultiplexingGraphD3SimulationRenderer.ts:
--------------------------------------------------------------------------------
1 | import QlogConnection from '@/data/Connection';
2 | import * as d3 from 'd3';
3 | import * as qlog from '@/data/QlogSchema';
4 | import StreamGraphDataHelper from './MultiplexingGraphDataHelper';
5 |
6 |
7 | export default class MultiplexingGraphD3SimulationRenderer {
8 |
9 | public containerID:string;
10 |
11 | public rendering:boolean = false;
12 |
13 | protected svg!:any;
14 | protected connection!:QlogConnection;
15 |
16 | protected barHeight = 50;
17 |
18 | private dimensions:any = {};
19 |
20 | constructor(containerID:string) {
21 | this.containerID = containerID;
22 | }
23 |
24 | public async render(connection:QlogConnection):Promise {
25 | if ( this.rendering ) {
26 | return false;
27 | }
28 |
29 | console.log("MultiplexingGraphD3SimulationRenderer:render", connection);
30 |
31 | this.rendering = true;
32 |
33 | const canContinue:boolean = this.setup(connection);
34 |
35 | if ( !canContinue ) {
36 | this.rendering = false;
37 |
38 | return false;
39 | }
40 |
41 | await this.renderLive();
42 | this.rendering = false;
43 |
44 | return true;
45 | }
46 |
47 | protected setup(connection:QlogConnection){
48 | this.connection = connection;
49 | this.connection.setupLookupTable();
50 |
51 | const container:HTMLElement = document.getElementById(this.containerID)!;
52 |
53 | this.dimensions.margin = {top: 20, right: Math.round(container.clientWidth * 0.05), bottom: 0, left: 5};
54 |
55 | // width and height are the INTERNAL widths (so without the margins)
56 | this.dimensions.width = container.clientWidth - this.dimensions.margin.left - this.dimensions.margin.right;
57 | this.dimensions.height = this.barHeight;
58 |
59 |
60 | // clear old rendering
61 | d3.select( "#" + this.containerID ).selectAll("*").remove();
62 |
63 | this.svg = d3.select("#" + this.containerID)
64 | .append("svg")
65 | .attr("width", this.dimensions.width + this.dimensions.margin.left + this.dimensions.margin.right)
66 | .attr("height", this.dimensions.height + this.dimensions.margin.top + this.dimensions.margin.bottom)
67 | // .attr("viewBox", [0, 0, this.dimensions.width, this.dimensions.height])
68 | .attr("xmlns", "http://www.w3.org/2000/svg")
69 | .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
70 | .attr("font-family", "Trebuchet-ms")
71 | .append("g")
72 | .attr("transform",
73 | "translate(" + this.dimensions.margin.left + "," + this.dimensions.margin.top + ")");
74 |
75 | return true;
76 | }
77 |
78 | protected async renderLive() {
79 | console.log("Rendering streamgraphrequests");
80 |
81 | const parser = this.connection.getEventParser();
82 |
83 | // want total millisecond range in this trace, so last - first
84 | const xMSRange = parser.load(this.connection.getEvents()[ this.connection.getEvents().length - 1 ]).absoluteTime -
85 | parser.load(this.connection.getEvents()[0]).absoluteTime;
86 |
87 | console.log("DEBUG MS range for this trace: ", xMSRange);
88 |
89 | let requestCount = 1;
90 |
91 | let eventType = qlog.HTTP3EventType.frame_created;
92 | if ( this.connection.vantagePoint && this.connection.vantagePoint.type === qlog.VantagePointType.server ){
93 | eventType = qlog.HTTP3EventType.frame_parsed;
94 | }
95 |
96 | const frames = this.connection.lookup( qlog.EventCategory.http, eventType );
97 |
98 | const requestsSent = [];
99 | for ( const eventRaw of frames ) {
100 | const evt = this.connection.parseEvent( eventRaw );
101 | const data = evt.data;
102 |
103 | if ( !data.frame || data.frame.frame_type !== qlog.HTTP3FrameTypeName.headers ) {
104 | continue;
105 | }
106 |
107 | if ( !StreamGraphDataHelper.isDataStream( "" + data.stream_id )) {
108 | // skip control streams like QPACK
109 | continue;
110 | }
111 |
112 | let method = undefined;
113 | let path = undefined;
114 | for ( const header of data.frame.headers ){
115 | if ( header.name === ":method" ){
116 | method = header.value;
117 | }
118 | if ( header.name === ":path" ){
119 | path = header.value;
120 | }
121 | }
122 |
123 | requestsSent.push( {streamID: data.stream_id, count: requestCount, text: method + " " + path } );
124 | ++requestCount;
125 | }
126 |
127 | // TODO: what if no H3-level stuff found?!
128 |
129 | const xDomain = d3.scaleLinear()
130 | .domain([1, requestCount])
131 | .range([ 0, this.dimensions.width ]);
132 |
133 | const xAxis = this.svg.append("g");
134 |
135 | // if( this.axisLocation === "top" ) {
136 | // xAxis
137 | // //.attr("transform", "translate(0," + this.dimensions.height + ")")
138 | // .call(d3.axisTop(xDomain));
139 | // }
140 | // else {
141 | // xAxis
142 | // .attr("transform", "translate(0," + this.dimensions.height + ")")
143 | // .call(d3.axisBottom(xDomain));
144 | // }
145 |
146 |
147 | console.log("DEBUG: requestsSent", requestsSent);
148 |
149 | // let colorDomain = d3.scaleOrdinal()
150 | // .domain(["0", "4", "8", "12", "16", "20", "24", "28", "32", "36", "40", "44"])
151 | // .range([ "red", "green", "blue", "pink", "purple", "yellow", "indigo", "black", "grey", "brown", "cyan", "orange"]);
152 |
153 | const clip = this.svg.append("defs").append("SVG:clipPath")
154 | .attr("id", "clip")
155 | .append("SVG:rect")
156 | .attr("width", this.dimensions.width )
157 | .attr("height", this.dimensions.height )
158 | .attr("x", 0)
159 | .attr("y", 0);
160 |
161 | const rects = this.svg.append('g')
162 | .attr("clip-path", "url(#clip)");
163 |
164 | const rects2 = rects
165 | .selectAll("rect")
166 | .data(requestsSent)
167 | .enter()
168 | .append("g");
169 |
170 |
171 | rects2
172 | // background
173 | .append("rect")
174 | .attr("x", (d:any) => { return xDomain(d.count) - 0.15; } )
175 | .attr("y", (d:any) => { return 0; } )
176 | // .attr("fill", (d:any) => { return "" + colorDomain( "" + d.streamID ); } )
177 | .attr("fill", (d:any) => { return StreamGraphDataHelper.StreamIDToColor("" + d.streamID)[0]; } )
178 | .style("opacity", 1)
179 | .attr("class", "packet")
180 | .attr("width", (d:any) => { return xDomain(d.count + 1) - xDomain(d.count) + 0.3; })
181 | .attr("height", this.barHeight);
182 | // .insert("rect")
183 | // .attr("x", (d:any) => { return xDomain(d.count) + 15; } )
184 | // .attr("y", (d:any) => { return 15; } )
185 | // .attr("fill", "#FFFFFF" )
186 | // .style("opacity", 0.75)
187 | // .attr("width", (d:any) => { return xDomain(d.count + 1) - xDomain(d.count) - 15; })
188 | // .attr("height", this.barHeight * 0.2 );
189 |
190 | rects2
191 | // .selectAll("rect")
192 | // .data(requestsSent)
193 | // .enter()
194 | // textbox
195 | .append("rect")
196 | .attr("x", (d:any) => { return xDomain(d.count) + 15; } )
197 | .attr("y", (d:any) => { return 15; } )
198 | .attr("fill", "#FFFFFF" )
199 | .style("opacity", 0.75)
200 | .attr("width", (d:any) => { return xDomain(d.count + 1) - xDomain(d.count) - 30; })
201 | .attr("height", this.barHeight * 0.4 );
202 |
203 | rects2
204 | // text
205 | .append("text")
206 | .attr("x", (d:any) => { return xDomain(d.count) + 15 + 2; } )
207 | .attr("y", (d:any) => { return 15 + ((this.barHeight * 0.4) / 2); } )
208 | .attr("dominant-baseline", "middle")
209 | .style("text-anchor", "start")
210 | .style("font-size", "14")
211 | .style("font-family", "Trebuchet MS")
212 | .style("font-weight", "bold")
213 | .attr("fill", "#000000")
214 | .text( (d:any) => { return d.text; } );
215 |
216 |
217 | // const updateChart = () => {
218 |
219 | // // recover the new scale
220 | // const newX = d3.event.transform.rescaleX(xDomain);
221 |
222 | // // update axes with these new boundaries
223 | // // xAxis./*transition().duration(200).*/call(d3.axisBottom(newX));
224 | // // if ( this.axisLocation === "top" ){
225 | // // xAxis.call(d3.axisTop(newX));
226 | // // }
227 | // // else {
228 | // // xAxis.call(d3.axisBottom(newX));
229 | // // }
230 |
231 | // // update circle position
232 | // rects
233 | // .selectAll(".packet")
234 | // // .transition().duration(200)
235 | // .attr("x", (d:any) => { return newX(d.count) - 0.15; } )
236 | // // .attr("y", (d:any) => { return 50; } )
237 | // .attr("width", (d:any) => { return xDomain(1) + 0.3; })
238 | // };
239 |
240 | // const zoom = d3.zoom()
241 | // .scaleExtent([1, 20]) // This control how much you can unzoom (x0.5) and zoom (x20)
242 | // .translateExtent([[0, 0], [this.dimensions.width, this.dimensions.height]])
243 | // .extent([[0, 0], [this.dimensions.width, this.dimensions.height]])
244 | // .on("zoom", updateChart);
245 |
246 | // // This add an invisible rect on top of the chart area. This rect can recover pointer events: necessary to understand when the user zoom
247 | // this.svg.append("rect")
248 | // .attr("width", this.dimensions.width)
249 | // .attr("height", this.dimensions.height)
250 | // .style("fill", "none")
251 | // .style("pointer-events", "all")
252 | // // .attr('transform', 'translate(' + 0 + ',' + this.dimensions.margin.top + ')')
253 | // .call(zoom);
254 | }
255 |
256 | }
257 |
--------------------------------------------------------------------------------
/visualizations/src/components/multiplexinggraph/renderer/MultiplexingGraphDataHelper.ts:
--------------------------------------------------------------------------------
1 | import ColorHelper from '@/components/shared/helpers/ColorHelper';
2 |
3 | export default class MultiplexingGraphDataHelper {
4 |
5 | public static isDataStream(streamID:string){
6 | return parseInt(streamID, 10) % 4 === 0;
7 | }
8 |
9 | public static StreamIDToColor(streamID:string):Array {
10 |
11 | return ColorHelper.StreamIDToColor( streamID, "HTTP3" );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/visualizations/src/components/packetizationdiagram/PacketizationDiagramConfigurator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Select a trace via the dropdown(s) below to visualize it in the packetization diagram
7 |
8 |
9 |
10 |
11 |
12 |
24 |
25 |
26 |
27 |
28 | More info on this tool
29 |
30 |
31 |
32 |
33 | Please load a trace file to visualize it
34 | Loading files...
35 |
36 |
37 | How to test?
38 | Load the predefined DEMO files (using the "manage files" tab above) and then select the "DEMO_10_parallel_streams " file here (the other demo files are a bit flaky on this visualization because they're older and don't always contain all the necessary fields)
39 |
40 | You can also upload your own qlog files, but note that this has been tested mainly on client-side traces. Server-side logs should work, but there might be dragons. Let us know if you find any bugs.
41 |
42 | What does it do?
43 |
44 | The packetization diagram shows how QUIC and HTTP/3 frames are packed inside each other and inside QUIC packets.
45 | This helps to see how (especially STREAM and DATA) frames are sized, how packet sizes are modulated, if HTTP/3 frame boundaries don't directly correspond to QUIC boundaries, etc.
46 |
47 |
48 | The bottom row (black and grey) shows the QUIC packets. Each packet has a header (the lower area) and a payload (the higher area).
49 | The first packet is black, the second grey, etc. to clearly show when a new packet starts. This use of alternating colors to show clear delineations is consistent for the rest of the diagram as well.
50 |
51 |
52 | The second row (red and pink) shows the QUIC frames inside the packet payloads. Each frame has a frame header and a payload.
53 |
54 | The third row (blue and light blue) show the HTTP/3 frames inside QUIC STREAM frame's payloads. Each HTTP/3 frame has a frame header and a payload.
55 |
56 | The fourth row (several colors) show to which stream each HTTP/3 frame belongs (if any). This to make it easier to track how streams are multiplexed on the wire.
57 |
58 |
59 | IMPORTANT: the x-axis does NOT show time but bytes sent/received .
60 | This also means that the two individual x-axis (for the top and bottom diagrams) do not (directly) overlap in time, even though it might appear like that at first glance!
61 | To get a better idea of timing, use the sequence diagram
62 |
63 | You can hover over each element and get a popup with more in-context information on what's present at that location.
64 | You can also zoom with the scrollwheel and drag to pan the diagram.
65 |
66 | Keep an eye on the browser devtools' JavaScript console: if you see weird behaviour, you'll probably see an error message explaining things there as well.
67 |
68 | How does it work?
69 |
70 | The visualization currently looks mainly at the following qlog events and attributes to figure out packet and frame sizes:
71 |
72 | packet_sent/packet_received : raw : length
73 | packet_sent/packet_received : frames[ StreamFrame ] : length
74 | frame_parsed/frame_created : byte_length
75 |
76 |
77 | However, these events are not enough to fully know the sizes of all QUIC frames: we only have explicit (payload) lengths for the stream frames.
78 | Because of QUIC's use of variable-length integer encoding for frame headers, it's difficult to know the frame's header and payload sizes (especially for things like ACK frames).
79 | qlog draft-02 should include fields to allow explicit logging of these lengths. For pre-02, this tool tries to guesstimate the sizes (and probably gets it wrong).
80 | If we fail to correctly guess, we backfill with bright yellow rendered "filler" frames.
81 |
82 | Note that this means that while the sizes of most QUIC frames (except STREAM frames) are off, the visualization is still accurate in showing which frames were in which packet.
83 |
84 | Also usable for TCP + TLS + HTTP/2
85 |
86 | This visualization was first created for use with TCP + TLS + HTTP/2, as there the implementations are typically less well aggregated. This often leads to inefficiencies in how data is packed inside the different layers.
87 | If you want to use this tool with those protocols as well, create a decrypted trace (so HTTP/2 is decoded) using wireshark (or equivalent) and then output it as JSON (either from wireshark directly or using tshark).
88 | You can then upload that as a .json file using the qvis file manager. It has a built-in pcap-to-qlog convertor (or at least the basic of one for this purpose).
89 | See the qvis repository for a demo TCP file.
90 |
91 |
92 | Close
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
109 |
110 |
190 |
--------------------------------------------------------------------------------
/visualizations/src/components/packetizationdiagram/PacketizationDiagramContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
33 |
--------------------------------------------------------------------------------
/visualizations/src/components/packetizationdiagram/PacketizationDiagramRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/visualizations/src/components/packetizationdiagram/data/PacketizationDiagramConfig.ts:
--------------------------------------------------------------------------------
1 | import Connection from "@/data/Connection";
2 |
3 | export default class PacketizationDiagramConfig {
4 |
5 | public collapsed:boolean = true;
6 | public connections:Array = new Array();
7 | }
8 |
--------------------------------------------------------------------------------
/visualizations/src/components/packetizationdiagram/renderer/PacketizationDiagramDataHelper.ts:
--------------------------------------------------------------------------------
1 | import ColorHelper from '@/components/shared/helpers/ColorHelper';
2 |
3 | export default class PacketizationDiagramDataHelper {
4 |
5 | public static StreamIDToColor(streamID:string, protocol:"HTTP2"|"HTTP3"):Array {
6 |
7 | return ColorHelper.StreamIDToColor( streamID, protocol );
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/visualizations/src/components/packetizationdiagram/renderer/PacketizationDiagramModels.ts:
--------------------------------------------------------------------------------
1 | import * as qlog from '@/data/QlogSchema';
2 |
3 | export enum PacketizationDirection {
4 | sending,
5 | receiving,
6 | }
7 |
8 | export interface PacketizationRange {
9 | start:number,
10 | size:number,
11 |
12 | isPayload:boolean,
13 | contentType?:string, // e.g., header frame, payload frame, application record, ...
14 |
15 | index:number,
16 | lowerLayerIndex:number, // for easier correlation with lower layer ranges
17 |
18 | color:string,
19 |
20 | rawPacket?:any, // the raw qlog event
21 | extra?:any // extra info needed to stringify-this
22 | }
23 |
24 | export interface PacketizationLane {
25 | name:string, // showed next to the lane, purely visual
26 | CSSClassName:string, // CSS class used for items on this lane
27 | rangeToString:(r:PacketizationRange) => string, // used when hovering over a range to show additional information
28 |
29 | heightModifier?:number, // mainly to reduce the height of an individual lane
30 |
31 | ranges:Array, // the actual ranges to be drawn in the lane
32 |
33 | max_size_local?:number,
34 | max_size_remote?:number,
35 |
36 | efficiency?:number
37 | }
38 |
39 |
40 | export interface LightweightRange {
41 | start: number,
42 | size: number
43 | }
44 |
45 | export class PacketizationPreprocessor {
46 |
47 | // FIXME: refactor this into a general HTTP3 protocol helper class
48 | public static H3FrameTypeToNumber(frame:any) {
49 | /* // draft-27
50 | +--------------+-------+---------------+
51 | | Frame Type | Value | Specification |
52 | +==============+=======+===============+
53 | | DATA | 0x0 | Section 7.2.1 |
54 | +--------------+-------+---------------+
55 | | HEADERS | 0x1 | Section 7.2.2 |
56 | +--------------+-------+---------------+
57 | | Reserved | 0x2 | N/A |
58 | +--------------+-------+---------------+
59 | | CANCEL_PUSH | 0x3 | Section 7.2.3 |
60 | +--------------+-------+---------------+
61 | | SETTINGS | 0x4 | Section 7.2.4 |
62 | +--------------+-------+---------------+
63 | | PUSH_PROMISE | 0x5 | Section 7.2.5 |
64 | +--------------+-------+---------------+
65 | | Reserved | 0x6 | N/A |
66 | +--------------+-------+---------------+
67 | | GOAWAY | 0x7 | Section 7.2.6 |
68 | +--------------+-------+---------------+
69 | | Reserved | 0x8 | N/A |
70 | +--------------+-------+---------------+
71 | | Reserved | 0x9 | N/A |
72 | +--------------+-------+---------------+
73 | | MAX_PUSH_ID | 0xD | Section 7.2.7 |
74 | +--------------+-------+---------------+
75 | */
76 | switch ( frame.frame_type ) {
77 | case qlog.HTTP3FrameTypeName.data:
78 | return 0x00;
79 | break;
80 | case qlog.HTTP3FrameTypeName.headers:
81 | return 0x01;
82 | break;
83 | case qlog.HTTP3FrameTypeName.reserved:
84 | return 0x02;
85 | break;
86 | case qlog.HTTP3FrameTypeName.cancel_push:
87 | return 0x03;
88 | break;
89 | case qlog.HTTP3FrameTypeName.settings:
90 | return 0x04;
91 | break;
92 | case qlog.HTTP3FrameTypeName.push_promise:
93 | return 0x05;
94 | break;
95 | case qlog.HTTP3FrameTypeName.goaway:
96 | return 0x07;
97 | break;
98 | case qlog.HTTP3FrameTypeName.max_push_id:
99 | return 0x0D;
100 | break;
101 | case qlog.HTTP3FrameTypeName.unknown:
102 | return frame.raw_frame_type;
103 | break;
104 | default:
105 | return 0x00;
106 | break;
107 | }
108 | }
109 |
110 | // FIXME: refactor this into a general QUIC protocol helper class
111 | public static VLIELength(input:number) {
112 | /*
113 | +------+--------+-------------+-----------------------+
114 | | 2Bit | Length | Usable Bits | Range |
115 | +======+========+=============+=======================+
116 | | 00 | 1 | 6 | 0-63 |
117 | +------+--------+-------------+-----------------------+
118 | | 01 | 2 | 14 | 0-16383 |
119 | +------+--------+-------------+-----------------------+
120 | | 10 | 4 | 30 | 0-1073741823 |
121 | +------+--------+-------------+-----------------------+
122 | | 11 | 8 | 62 | 0-4611686018427387903 |
123 | +------+--------+-------------+-----------------------+
124 | */
125 |
126 | if ( input <= 63 ) {
127 | return 1;
128 | }
129 | if ( input <= 16383 ) {
130 | return 2;
131 | }
132 | if ( input <= 1073741823 ) {
133 | return 4;
134 | }
135 |
136 | return 8;
137 | }
138 |
139 | public static extractRanges(ranges:Array, size:number) {
140 | const output:Array = new Array();
141 |
142 | let remainingLength = size;
143 |
144 | if ( size === 0 ) {
145 | console.warn("Trying to extract ranges for size 0... potential error? Skipping...");
146 |
147 | return output;
148 | }
149 |
150 | while ( ranges.length > 0 ) {
151 | const range = ranges.shift();
152 |
153 | // console.log("Considering range", range, remainingLength);
154 |
155 | // either we consume the range, or we need to split it
156 | // the last option should only happen once at maximum, at the very end of this run
157 | if ( range!.start + range!.size <= range!.start + remainingLength ) {
158 | // full range is consumed
159 | // console.log("Consuming range!", range!.start, range!.size, remainingLength );
160 | output.push( range! );
161 | }
162 | else {
163 | // console.log("Splitting range!", range!.start, range!.size, remainingLength );
164 |
165 | if ( size === 5 && remainingLength < 5 ) { // header is being split... bad for performance
166 | console.warn("Splitting a header range... server is being bad/naive?", size);
167 | }
168 |
169 | // range needs to be split
170 | ranges.unshift( {start: range!.start + remainingLength, size: range!.size - remainingLength} );
171 | range!.size = remainingLength; // this struct isn't added back to the "ranges" array, so can safely change it for use below
172 |
173 | // console.log("We split the range", range);
174 |
175 | output.push( range! );
176 | }
177 |
178 | if ( range!.size < 0 ) { // sanity check
179 | console.error("PacketizationDiagram:extractRanges : Negative size after extracting ranges! Should not happen!", range!.size, range, ranges);
180 | }
181 |
182 | remainingLength -= range!.size;
183 |
184 | // console.log("Remaining length", remainingLength, range!.size);
185 |
186 | if ( remainingLength < 0 ) { // sanity check
187 | alert("Remaining length < 0, CANNOT HAPPEN! " + remainingLength); // FIXME: make alert
188 | break;
189 | }
190 |
191 | if ( remainingLength === 0 ) {
192 | break;
193 | }
194 | }
195 |
196 | if ( remainingLength !== 0 ) {
197 | alert("Trying to fill payloadranges that aren't there! " + remainingLength); // FIXME: make alert
198 | }
199 |
200 | return output;
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/visualizations/src/components/sequencediagram/SequenceDiagramContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
47 |
--------------------------------------------------------------------------------
/visualizations/src/components/sequencediagram/SequenceDiagramRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 | {{ eventDetail }}
17 |
18 | Value of all recovery metrics at this point:
19 | {{ eventDetailExtra }}
20 | Close
21 |
22 |
23 |
24 |
25 |
30 |
31 |
--------------------------------------------------------------------------------
/visualizations/src/components/sequencediagram/SequenceDiagramSimpleRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
ManualRTT: {{config.manualRTT}}
4 |
Scale: {{config.scale}}
5 |
6 |
7 |
8 |
9 | - {{index}} : {{connection.name}} ( {{connection.parent.description}} )
10 |
11 | = {{index}} : {{connection.parseEvent(event).time}} {{connection.parseEvent(event).category}} {{connection.parseEvent(event).name}} {{connection.parseEvent(event).trigger}} {{(connection.parseEvent(event).data && connection.parseEvent(event).data.header) ? connection.parseEvent(event).data.header.version : ""}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/visualizations/src/components/sequencediagram/data/SequenceDiagramConfig.ts:
--------------------------------------------------------------------------------
1 | import Connection from "@/data/Connection";
2 |
3 | // we keep track of the timeOffset separately here for two reasons:
4 | // 1. changing it directly in EventParser wouldn't be reactive by itself, we would have to trigger UI updates another way
5 | // 2. we don't want the global timeOffset to change for all visualizations (e.g., you probably don't want this for the congestion graph)
6 | // Separate tracking allows reactivity, but adds other complexities (e.g., more difficult to persist the offset when changing views etc.)
7 | // Main issue: the timeOffset is forgotten when switching connections in SequenceDiagramConfigurator, since that works on the raw Connection via ConnectionConfigurator...
8 | // as such, we now keep a copy of the timeOffset and some -dirty dirty dirty- persistence code in SequenceDiagramD3Renderer to circumvent this
9 | // TODO: revisit this whole setup to see if something simpler isn't possible
10 | export interface SequenceDiagramConnection {
11 | connection:Connection,
12 | timeOffset:number
13 | }
14 |
15 | export default class SequenceDiagramConfig {
16 |
17 | public static createConnectionWithTimeoffset(connection:Connection):SequenceDiagramConnection {
18 | return {
19 | connection: connection,
20 | timeOffset: connection.getEventParser() ? connection.getEventParser().timeOffset : 0,
21 | }
22 | }
23 |
24 | // public scale:number = 0.1; // amount of pixels per ms // by default 1 pixel = 10 ms // can be in [0,1[ to squish things
25 |
26 | // the connections to be shown in the SequenceDiagram
27 | // index 0 = left, index 1 = next one to the right, ... , length - 1 = rightmost one
28 | public connections:Array = new Array();
29 |
30 | public timeResolution:number = 1; // for dealing with traces with sub-millisecond latencies (as the sequence diagram groups per millisecond)
31 | }
32 |
--------------------------------------------------------------------------------
/visualizations/src/components/shared/ConnectionConfigurator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | −
18 |
19 |
20 |
21 |
22 |
23 | {{selectedConnection.parent.filename}} ({{selectedConnection.parent.description}})
24 | {{numericalInputName}} :
25 |
26 |
27 |
28 |
29 |
30 | −
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/visualizations/src/components/shared/helpers/ColorHelper.ts:
--------------------------------------------------------------------------------
1 | export default class ColorHelper {
2 |
3 | public static StreamIDToColor(streamID:string, protocol:"HTTP2"|"HTTP3"):Array {
4 |
5 | let lut;
6 | if ( protocol === "HTTP2" ) {
7 | lut = ColorHelper.H2streamIDColorLUT;
8 | }
9 | else {
10 | lut = ColorHelper.H3streamIDColorLUT;
11 | }
12 |
13 | // colors inspired by http://artshacker.com/wp-content/uploads/2014/12/Kellys-22-colour-chart.jpg
14 |
15 | if ( protocol === "HTTP2" ) {
16 | if ( lut.size === 0 ){
17 | lut.set( "0", "#FF0000" ); // bright red
18 | lut.set( "1", "#E1DA4C" ); // yellow
19 | lut.set( "3", "#6B067F" ); // purple
20 | lut.set( "5", "#E17426" ); // orange
21 | lut.set( "7", "#914ca8" ); // purple
22 | lut.set( "9", "#99CBDF" ); // light blue
23 | lut.set( "11", "#C72737" ); // red
24 | lut.set( "13", "#BBC585" ); // buff
25 | lut.set( "15", "#7D787A" ); // grey
26 | lut.set( "17", "#6CAD58" ); // green
27 | lut.set( "19", "#D580AA" ); // pink
28 | lut.set( "21", "#4C61B9" ); // blue
29 | lut.set( "23", "#D38E75" ); // yellowish pink
30 | lut.set( "25", "#46198C" ); // violet
31 | lut.set( "27", "#C8A454" ); // oker
32 | }
33 |
34 | if ( lut.has(streamID) ) {
35 | return [ lut.get( streamID )!, "black" ];
36 | }
37 | else {
38 | let streamIDnumber = parseInt( streamID, 10 );
39 |
40 | if ( streamIDnumber > 27 ) {
41 | streamIDnumber = streamIDnumber % 28;
42 | }
43 |
44 | if ( lut.has("" + streamIDnumber ) ) {
45 | return [ lut.get( "" + streamIDnumber )!, "black" ];
46 | }
47 | else {
48 | return [ "lavenderblush", "black" ];
49 | }
50 | }
51 | }
52 | else {
53 | if ( lut.size === 0 ){
54 | lut.set( "0", "#E1DA4C" ); // yellow
55 | lut.set( "4", "#6B067F" ); // purple
56 | lut.set( "8", "#E17426" ); // orange
57 | lut.set( "12", "#914ca8" ); // purple
58 | lut.set( "16", "#99CBDF" ); // light blue
59 | lut.set( "20", "#C72737" ); // red
60 | lut.set( "24", "#BBC585" ); // buff
61 | lut.set( "28", "#7D787A" ); // grey
62 | lut.set( "32", "#6CAD58" ); // green
63 | lut.set( "36", "#D580AA" ); // pink
64 | lut.set( "40", "#4C61B9" ); // blue
65 | lut.set( "44", "#D38E75" ); // yellowish pink
66 | lut.set( "48", "#46198C" ); // violet
67 | lut.set( "52", "#C8A454" ); // oker
68 | }
69 |
70 | if ( lut.has(streamID) ) {
71 | return [ lut.get( streamID )!, "black" ];
72 | }
73 | else {
74 | let streamIDnumber = parseInt( streamID, 10 );
75 |
76 | if ( streamIDnumber > 52 ) {
77 | streamIDnumber = streamIDnumber % 56;
78 | }
79 |
80 | if ( lut.has("" + streamIDnumber ) ) {
81 | return [ lut.get( "" + streamIDnumber )!, "black" ];
82 | }
83 | else {
84 | return [ "lavenderblush", "black" ];
85 | }
86 | }
87 | }
88 | }
89 |
90 | protected static H2streamIDColorLUT:Map = new Map();
91 | protected static H3streamIDColorLUT:Map = new Map();
92 | }
93 |
--------------------------------------------------------------------------------
/visualizations/src/components/stats/StatisticsConfigurator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Select a file via the dropdown(s) below to view its statistics
7 |
8 |
9 |
10 |
11 |
12 | Please load a trace file to visualize it
13 | Loading files...
14 |
15 |
16 |
17 |
18 |
19 |
28 |
29 |
77 |
--------------------------------------------------------------------------------
/visualizations/src/components/stats/StatisticsContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
39 |
--------------------------------------------------------------------------------
/visualizations/src/components/stats/StatisticsRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
File info
7 |
8 |
9 |
10 |
11 | Aspect
12 | Value
13 |
14 |
15 |
16 |
17 | Filename
18 | {{group.filename}}
19 |
20 |
21 | Title
22 | {{group.title}}
23 |
24 |
25 | Description
26 | {{group.description}}
27 |
28 |
29 | qlog version
30 | {{group.version}}
31 |
32 |
33 | Summary
34 | {{JSON.stringify(group.summary, null, 4)}}
35 |
36 |
37 | Trace count
38 | {{group.getConnections().filter( (c) => !c.wasAutoGenerated ).length}}
39 |
40 |
41 | Total event count
42 | {{totalEventCount}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
61 |
62 |
104 |
--------------------------------------------------------------------------------
/visualizations/src/components/stats/data/StatisticsConfig.ts:
--------------------------------------------------------------------------------
1 | import ConnectionGroup from "@/data/ConnectionGroup";
2 |
3 | export default class StatisticsConfig {
4 | // PROPERTIES MUST BE INITIALISED
5 | // OTHERWISE VUE DOES NOT MAKE THEM REACTIVE
6 | // !!!!!
7 |
8 | public group:ConnectionGroup | undefined = undefined;
9 | }
10 |
--------------------------------------------------------------------------------
/visualizations/src/data/Connection.ts:
--------------------------------------------------------------------------------
1 | import QlogConnectionGroup from '@/data/ConnectionGroup';
2 | import { IQlogEventParser, IQlogRawEvent } from '@/data/QlogEventParser';
3 | import * as qlog from '@/data/QlogSchema';
4 | import Vue from 'vue';
5 |
6 | // a single trace
7 | export default class QlogConnection {
8 |
9 |
10 | public parent:QlogConnectionGroup;
11 | public title:string;
12 | public description:string;
13 |
14 | public eventFieldNames:Array = new Array();
15 | public commonFields:qlog.ICommonFields = {};
16 | public configuration:qlog.IConfiguration = { time_offset: "0", time_units: "ms", original_uris: [] };
17 |
18 | public vantagePoint!:qlog.IVantagePoint;
19 |
20 | public wasAutoGenerated:boolean = false;
21 |
22 | private events:Array;
23 |
24 | private lookupTable:Map>>;
25 |
26 | // The EventParser is needed because qlog events aren't always of the same shape
27 | // They are also defined as flat arrays, with their member names defined separately (in event_fields)
28 | // As such, it is not immediately clear which of the indices in the flat array leads to which property (e.g., the timestamp is -usually- at 0, but could be anywhere)
29 | // So, the eventParser classes deal with this: figure out dynamically which index means what. We can then lookup the index by doing parser.load(event).propertyName
30 | private eventParser!:IQlogEventParser;
31 |
32 | public constructor(parent:QlogConnectionGroup) {
33 | this.parent = parent;
34 | this.title = "";
35 | this.description = "";
36 | this.events = new Array();
37 |
38 | this.vantagePoint = {
39 | type: qlog.VantagePointType.unknown,
40 | flow: qlog.VantagePointType.unknown,
41 | };
42 |
43 | (this.events as any)._isVue = true;
44 |
45 | this.parent.addConnection( this );
46 |
47 | this.lookupTable = new Map>();
48 | (this.lookupTable as any)._isVue = true;
49 | }
50 |
51 | // performs a DEEP clone of this connection
52 | // NOTE: this is SLOW and should only be used sparingly (mainly added for the sequence diagram)
53 | public clone():QlogConnection {
54 | // TODO: maybe find a better way to do this than just JSON.stringify?
55 | // online they recommend lodash's deepClone
56 | const output:QlogConnection = new QlogConnection( this.parent );
57 |
58 | output.title = this.title;
59 | output.description = this.description;
60 | output.eventFieldNames = this.eventFieldNames.slice();
61 | output.commonFields = JSON.parse( JSON.stringify(this.commonFields || []) );
62 | output.configuration = JSON.parse( JSON.stringify(this.configuration || {}) );
63 | output.vantagePoint = JSON.parse( JSON.stringify(this.vantagePoint || {}) );
64 | const events = JSON.parse( JSON.stringify(this.events || []) );
65 | (events as any)._isVue = true;
66 | output.events = events;
67 |
68 | output.eventParser = this.eventParser; // TODO: properly clone this one as well! should work for now, since it's supposed to be static
69 |
70 | return output;
71 | }
72 |
73 | public setEventParser( parser:IQlogEventParser ){
74 | // we need to bypass Vue's reactivity here
75 | // this Connection class is made reactive in ConnectionStore, including the this.eventParser property and its internals
76 | // however, if we use parseEvent(), this will update the internal .currentEvent property of this.eventParser
77 | // That update reactively triggers an update...
78 | // SO: if we would do {{ connection.parseEvent(evt).name }} inside the template (which is like... the main use case here)
79 | // then we get an infinite loop of reactivity (parseEvent() triggers update, update is rendered, template calls parseEvent() again, etc.)
80 |
81 | // Addittionally, we also don't want the full qlog file to be reactive: just the top-level stuff like the iQlog and the traces
82 | // the individual events SHOULD NOT be reactive:
83 | // 1) because they probably won't change
84 | // 2) because they can be huge and it would get very slow with the way Vue does observability (adding an __ob__ Observer class to EACH object and overriding getters/setters for everything)
85 |
86 | // We looked at many ways of doing this, most of which are discussed in the following issue:
87 | // https://github.com/vuejs/vue/issues/2637
88 | // In the end, the only thing that really worked for this specific setup is the ._isVue method
89 | // We use this both for eventParser and events and for the current setup, it seems to prevent both the infinite loop and event objects being marked as Observable
90 | // Obviously this is an ugly hack, but since Vue doesn't include a way to do this natively, I really don't see a better way...
91 |
92 | (parser as any)._isVue = true; // prevent the parser from being Vue Reactive
93 | this.eventParser = parser;
94 | this.eventParser.init( this );
95 | }
96 |
97 | // NOTE: only use this directly when connection.parseEvent() is too slow due to Vue's ReactiveGetter on it
98 | // see SequenceDiagramD3Renderer.calculateConnections for an example of that
99 | public getEventParser(){
100 | return this.eventParser;
101 | }
102 |
103 | public parseEvent( evt:IQlogRawEvent ){
104 | return this.eventParser.load( evt );
105 | }
106 |
107 | public setEvents(events:Array>):void {
108 | (events as any)._isVue = true; // prevent the individual events from being Vue Reactive, see above
109 | this.events = events;
110 | }
111 | public getEvents():Array> { return this.events; }
112 |
113 | public getVantagePointPerspective() {
114 | if ( this.vantagePoint.type === qlog.VantagePointType.server ||
115 | this.vantagePoint.type === qlog.VantagePointType.client ) {
116 | return this.vantagePoint.type;
117 | }
118 | else{
119 | // either network or unknown
120 | // for both, we look towards the .flow for guidance
121 | // according to the spec, there shouldn't be a flow for "unknown", but let's be robust shall we
122 | return this.vantagePoint.flow ? this.vantagePoint.flow : qlog.VantagePointType.unknown;
123 | }
124 | }
125 |
126 | public setupLookupTable() {
127 | if ( this.lookupTable.size !== 0 ){
128 | return;
129 | }
130 |
131 | for ( const evt of this.events ){
132 | const category = this.parseEvent(evt).category;
133 | const eventType = this.parseEvent(evt).name;
134 |
135 | if ( !this.lookupTable.has(category) ) {
136 | this.lookupTable.set( category, new Map>() );
137 | }
138 |
139 | const categoryDictionary = this.lookupTable.get(category);
140 | if ( !categoryDictionary!.has(eventType) ) {
141 | categoryDictionary!.set( eventType, new Array() );
142 | }
143 |
144 | categoryDictionary!.get(eventType)!.push( evt );
145 | }
146 | }
147 |
148 | public getLookupTable(){
149 | this.setupLookupTable();
150 |
151 | return this.lookupTable;
152 | }
153 |
154 | public lookup(category: qlog.EventCategory | string, eventType: qlog.EventType | string):Array {
155 | if ( this.lookupTable.has(category) && this.lookupTable.get(category)!.has(eventType) ){
156 | return this.lookupTable.get(category)!.get(eventType)!;
157 | }
158 | else {
159 | return [];
160 | }
161 | }
162 |
163 | public getLongName(){
164 | let connectionName = "";
165 | if ( this.vantagePoint ){
166 | if (this.vantagePoint.name){
167 | connectionName += this.vantagePoint.name + " : ";
168 | }
169 | if (this.vantagePoint.type){
170 | connectionName += this.vantagePoint.type;
171 | }
172 | else {
173 | connectionName += "UNKNOWN";
174 | }
175 |
176 | connectionName += (this.vantagePoint && this.vantagePoint.flow) ? " (flow = " + this.vantagePoint.flow + ") : " : " : ";
177 | }
178 | if ( this.title ) {
179 | connectionName += this.title;
180 | }
181 | if (this.description) {
182 | connectionName += " : " + this.description;
183 | }
184 |
185 | return connectionName;
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/visualizations/src/data/ConnectionGroup.ts:
--------------------------------------------------------------------------------
1 | import QlogConnection from "@/data/Connection"
2 |
3 | // This is basically the wrapper for a single qlog file, which contains multiple traces ("connections")
4 | // NOTE: this has nothing directly to do with the "group_id" concept!
5 | // TODO: rename this class to QlogCollection or QlogTraceCollection or QlogTraceFile or something
6 | export default class QlogConnectionGroup {
7 |
8 | public version:string;
9 | public format:string;
10 |
11 | public filename:string;
12 | public title:string;
13 | public description:string;
14 |
15 | public URL:string;
16 | public URLshort:string;
17 |
18 | public summary:any;
19 |
20 | private connections:Array;
21 |
22 | public constructor() {
23 | this.connections = new Array();
24 | this.version = "";
25 | this.format = "JSON";
26 | this.filename = "";
27 | this.URL = "";
28 | this.URLshort = "";
29 | this.title = "";
30 | this.description = "";
31 | this.summary = {};
32 | }
33 |
34 | public addConnection( connection:QlogConnection ):void { this.connections.push( connection ); }
35 | public getConnections() { return this.connections; }
36 |
37 | public getShorthand(){
38 | let output = "";
39 |
40 | if ( this.title ) {
41 | output += this.title;
42 | }
43 | if (this.description) {
44 | if ( this.description.length <= 50 ) {
45 | output += " : " + this.description;
46 | }
47 | else {
48 | output += " : " + this.description.substr(0,50) + "...";
49 | }
50 | }
51 |
52 | if ( this.connections.length !== 1 ) {
53 | output += " (" + this.connections.length + " connections)";
54 | }
55 | else {
56 | output += " (1 connection)";
57 | }
58 |
59 | return output;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/visualizations/src/data/QlogEventParser.ts:
--------------------------------------------------------------------------------
1 | import QlogConnection from '@/data/Connection';
2 |
3 | export enum TimeTrackingMethod {
4 | ABSOLUTE_TIME,
5 | RELATIVE_TIME,
6 | DELTA_TIME,
7 | }
8 |
9 | export interface IQlogEventParser {
10 |
11 | readonly relativeTime:number;
12 | readonly absoluteTime:number;
13 | readonly category:string;
14 | name:string; // name is not a readonly since we want to be able to change it when cloning traces (e.g., in sequenceDiagram)
15 | readonly data:any|undefined;
16 |
17 | readonly timeOffset:number;
18 |
19 | timeToMilliseconds(time: string | number): number;
20 | getAbsoluteStartTime(): number;
21 |
22 | init( connection:QlogConnection) : void;
23 | load( evt:IQlogRawEvent ) : IQlogEventParser;
24 |
25 | getTimeTrackingMethod() : TimeTrackingMethod;
26 | setReferenceTime(time:number) : void; // should not be needed normally
27 | }
28 |
29 | export type IQlogRawEvent = Array;
30 |
--------------------------------------------------------------------------------
/visualizations/src/data/QlogSchema.ts:
--------------------------------------------------------------------------------
1 | // this acts as a general import point for other scripts
2 | // we can then dynamically change here what we actually define so we can gradually upgrade from one version of qlog to another
3 |
4 | export * from "./QlogSchema02"
5 |
--------------------------------------------------------------------------------
/visualizations/src/data/QlogSchema01.ts:
--------------------------------------------------------------------------------
1 | export * from "@quictools/qlog-schema/draft-01/QLog";
2 |
--------------------------------------------------------------------------------
/visualizations/src/data/QlogSchemaConverter.ts:
--------------------------------------------------------------------------------
1 | import QlogConnectionGroup from './ConnectionGroup';
2 | import * as qlog01 from "@/data/QlogSchema01";
3 | import * as qlog02 from "@/data/QlogSchema02";
4 |
5 | export class QlogSchemaConverter {
6 |
7 | public static Convert01to02(input:QlogConnectionGroup):qlog02.IQLog {
8 |
9 | console.log("QlogConverter:Convert01to02 : Transforming draft-01 qlog file to draft-02 qlog", input);
10 |
11 | const output:qlog02.IQLog = {
12 | qlog_version: qlog02.Defaults.versionName,
13 | qlog_format: qlog02.LogFormat.JSON,
14 |
15 | title: input.title,
16 | description: input.description,
17 |
18 | summary: input.summary,
19 |
20 | traces: [],
21 | };
22 |
23 | output.traces = [];
24 |
25 | for ( const connection of input.getConnections() ) {
26 |
27 | if ( connection.wasAutoGenerated ) {
28 | // don't want to include auto-generated stuff (typically from the sequence diagram) in this output
29 | continue;
30 | }
31 |
32 | const newTrace:qlog02.ITrace = {
33 | vantage_point: connection.vantagePoint,
34 | title: connection.title,
35 | description: connection.description,
36 |
37 | configuration: connection.configuration,
38 | common_fields: connection.commonFields !== undefined ? connection.commonFields : {},
39 |
40 | events: [],
41 | };
42 |
43 | const event_fields = connection.eventFieldNames;
44 |
45 | if ( event_fields === undefined || event_fields.length === 0 ) {
46 |
47 | if ( qlog02.Defaults.versionAliases.indexOf(input.version) >= 0 ) {
48 | // already proper draft-02 trace, nothing to be done
49 |
50 | console.warn("QlogConverter:Convert01to02 : trace was already draft-02, just using existing events", connection.getEvents());
51 |
52 | newTrace.events = connection.getEvents() as any; // we're already draft-02, so this should be a proper array of raw qlog02.IEvent here, even though the type is still any[][]
53 | output.traces.push( newTrace );
54 | continue;
55 | }
56 | else {
57 |
58 | console.error("QlogConverter:Convert01to02 : no event_fields found, shouldn't happen! Skipping", connection);
59 | continue;
60 | }
61 | }
62 |
63 | output.traces.push( newTrace );
64 |
65 | // time format used to be implied from the name of the field in event_fields, now as the value of common_fields.time_format
66 | // note that the relative reference_time was already in common_fields, so no transformation is needed
67 | let timeIndex = event_fields.indexOf("relative_time");
68 | if ( timeIndex === -1 ) {
69 | timeIndex = event_fields.indexOf("delta_time");
70 | if ( timeIndex === -1 ) {
71 | timeIndex = event_fields.indexOf("time");
72 | newTrace.common_fields!.time_format = qlog02.TimeFormat.absolute;
73 | }
74 | else {
75 | newTrace.common_fields!.time_format = qlog02.TimeFormat.delta;
76 | }
77 | }
78 | else {
79 | newTrace.common_fields!.time_format = qlog02.TimeFormat.relative;
80 | }
81 |
82 | // major change in draft-02 is getting rid of event_fields and using normal JSON field names instead
83 | const categoryIndex = event_fields.indexOf("category");
84 | let typeIndex = event_fields.indexOf("event_type");
85 | if ( typeIndex < 0 ) {
86 | typeIndex = event_fields.indexOf("event");
87 | }
88 | const dataIndex = event_fields.indexOf("data");
89 | const triggerIndex = event_fields.indexOf("trigger");
90 |
91 | if ( timeIndex < 0 || categoryIndex < 0 || typeIndex < 0 || dataIndex < 0 ) {
92 | console.error("QlogConverter:Convert01to02 : expected fields time/category/event/data not found, skipping. ", timeIndex, categoryIndex, typeIndex, dataIndex);
93 | continue;
94 | }
95 |
96 | for ( const event of connection.getEvents() ) {
97 |
98 | // could use EventParser here, but since we've looked up the event_fields indices ourselves, just use the raw data instead
99 |
100 | const newEvent:qlog02.IEvent = {
101 |
102 | time: event[ timeIndex ],
103 | name: event[ categoryIndex ] + ":" + event[ typeIndex ],
104 | data: event[ dataIndex ],
105 |
106 | };
107 |
108 | newEvent.data = QlogSchemaConverter.Convert01to02EventData( newEvent.data as qlog01.EventData );
109 |
110 | if ( triggerIndex >= 0 ) {
111 | (newEvent.data as any).trigger = event[ triggerIndex ];
112 | }
113 |
114 | newTrace.events.push( newEvent );
115 | }
116 | }
117 |
118 | // note: this is the raw JSON. to get a connectionGroup for some reason, use QlogLoaderV2 on this output
119 | return output;
120 | }
121 |
122 | private static Convert01to02EventData( input:qlog01.EventData ): qlog02.EventData {
123 |
124 | const output:qlog02.EventData = {};
125 |
126 | for ( const key of Object.keys(input) ) {
127 | (output as any)[key] = (input as any)[key];
128 | }
129 |
130 | const rawInput = input as any;
131 | const rawOutput = output as any;
132 |
133 | if ( rawInput.packet_type ) {
134 | if ( !rawOutput.header ) {
135 | rawOutput.header = {};
136 | }
137 |
138 | rawOutput.header.packet_type = rawInput.packet_type;
139 | delete rawOutput.packet_type; // to enforce proper draft-02 structure
140 | }
141 |
142 | if ( rawInput.header && rawInput.header.packet_size ) {
143 | if ( !rawOutput.raw ) {
144 | rawOutput.raw = {};
145 | }
146 |
147 | rawOutput.raw.length = rawInput.header.packet_size;
148 | delete rawInput.header.packet_size; // to enforce proper draft-02 structure
149 | }
150 |
151 | if ( rawInput.header && rawInput.header.payload_length ) {
152 | if ( !rawOutput.raw ) {
153 | rawOutput.raw = {};
154 | }
155 |
156 | rawOutput.raw.payload_length = rawInput.header.payload_length;
157 | delete rawInput.header.payload_length;
158 | }
159 |
160 | // TODO: add other 01 to 02 changes for individual events
161 |
162 | return output;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/visualizations/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import BootstrapVue from "bootstrap-vue";
3 | import Notifications from 'vue-notification'
4 | import App from "./App.vue";
5 | import router from "./router";
6 | import store from "./store";
7 |
8 | import "bootstrap/dist/css/bootstrap.css";
9 | import "bootstrap-vue/dist/bootstrap-vue.css";
10 |
11 | // const standaloneFiles:Array = [
12 | // "draft-00/example_github.qlog.js",
13 | // "draft-01/new_cid.qlog.js",
14 | // "draft-01/spin_bit.qlog.js",
15 | // "prespec/ngtcp2_pcap1.qlog.js",
16 | // /*
17 | // "prespec/quictracker_handshake_v6_quicker_20181219.qlog.js",
18 | // "prespec/ngtcp2_multistreams_server_noloss.qlog.js",
19 | // "prespec/ngtcp2_multistreams_server_10ploss.qlog.js",
20 | // "prespec/ngtcp2_multistreams_server_losscomparison.qlog.js",
21 | // */
22 | // ];
23 |
24 | // const connectionStore = getModule(ConnectionStore, store);
25 |
26 | // for ( const filepath of standaloneFiles ){
27 |
28 | // const scriptelement = document.createElement('script');
29 |
30 | // console.log("Loading ", filepath );
31 |
32 | // scriptelement.onload = () => {
33 | // // the standalone file has a single variable in it, named after the file, so we can get the contents
34 | // // e.g., var dupli_pkts_cl_ngtcp2 = {...}
35 | // // since it's a 'var' and not 'let', we can access it via the window[]
36 | // // prespec/ngtcp2_pcap1.qlog.js -> ngtcp2_pcap1
37 | // let varname = filepath.substr(filepath.indexOf("/") + 1);
38 | // varname = varname.substr(0, varname.indexOf("."));
39 |
40 | // // @ts-ignore
41 | // const file = window[varname];
42 | // // @ts-ignore
43 | // window[varname] = "loaded"; // make sure it can be gc'ed if necessary
44 |
45 | // connectionStore.addGroupFromQlogFile( {fileContentsJSON: file, filename: varname} ).then( () => {
46 | // console.log("Loaded ", varname, file );
47 | // });
48 | // };
49 |
50 | // scriptelement.onerror = (err) => {
51 | // console.error("Loading error ", filepath, err);
52 | // };
53 |
54 | // scriptelement.onabort = () => {
55 | // console.error("Loading aborted ", filepath);
56 | // };
57 |
58 | // scriptelement.src = "standalone_data/" + filepath;
59 | // document.head.appendChild(scriptelement);
60 | // }
61 |
62 |
63 |
64 | // TODO: REMOVE: only for local testing!
65 | // if (window.location.toString().indexOf(":8080") >= 0 && window.location.toString().indexOf("nodemo") < 0 ){
66 | // console.log("Autoloading demo files");
67 | // setTimeout( () => {
68 | // getModule(ConnectionStore, store).loadExamplesForDemo();
69 | // }, 500);
70 | // }
71 |
72 | Vue.config.productionTip = false;
73 | Vue.use(BootstrapVue);
74 | Vue.use(Notifications);
75 |
76 | new Vue({
77 | router,
78 | store,
79 | render: (h) => h(App),
80 | }).$mount("#app");
81 |
--------------------------------------------------------------------------------
/visualizations/src/router.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Router from "vue-router";
3 | import MainMenu from "./views/MainMenu.vue";
4 | import VUEDebug from "./views/VUEDebug.vue";
5 | import FileManager from "./views/FileManager.vue";
6 | import SequenceDiagram from "./views/SequenceDiagram.vue";
7 | import PacketizationDiagram from "./views/PacketizationDiagram.vue";
8 | import CongestionGraph from "./views/CongestionGraph.vue";
9 | import MultiplexingGraph from "./views/MultiplexingGraph.vue";
10 | import Statistics from "./views/Statistics.vue";
11 |
12 | Vue.use(Router);
13 |
14 | const router = new Router({
15 | routes: [
16 | {
17 | path: "/",
18 | redirect: "/files",
19 | },
20 | {
21 | path: "/debug",
22 | name: "VUEDebug",
23 | components: {
24 | default: VUEDebug,
25 | menu: MainMenu,
26 | },
27 | },
28 | {
29 | path: "/files",
30 | name: "FileManager",
31 | components: {
32 | default: FileManager,
33 | menu: MainMenu,
34 | },
35 | },
36 | {
37 | path: "/sequence",
38 | name: "sequence",
39 | // route level code-splitting
40 | // this generates a separate chunk (about.[hash].js) for this route
41 | // which is lazy-loaded when the route is visited.
42 | // component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
43 | components: {
44 | default: SequenceDiagram,
45 | menu: MainMenu,
46 | },
47 | },
48 | {
49 | path: "/congestion",
50 | name: "congestion",
51 | // route level code-splitting
52 | // this generates a separate chunk (about.[hash].js) for this route
53 | // which is lazy-loaded when the route is visited.
54 | // component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
55 | components: {
56 | default: CongestionGraph,
57 | menu: MainMenu,
58 | },
59 | },
60 | {
61 | path: "/multiplexing",
62 | name: "multiplexing",
63 | components: {
64 | default: MultiplexingGraph,
65 | menu: MainMenu,
66 | },
67 | },
68 | {
69 | path: "/packetization",
70 | name: "packetization",
71 | components: {
72 | default: PacketizationDiagram,
73 | menu: MainMenu,
74 | },
75 | },
76 | {
77 | path: "/stats",
78 | name: "Statistics",
79 | components: {
80 | default: Statistics,
81 | menu: MainMenu,
82 | },
83 | },
84 | ],
85 | });
86 |
87 | function hasQueryParams(route:any) {
88 | return !!Object.keys(route.query).length;
89 | }
90 |
91 | // Vue does something weird with its processing of query parameters
92 | // normally, we get an url like : mydomain.com/#/routename
93 | // if we then do mydomain.com/#/routename?param1=test, everything works
94 | // HOWEVER
95 | // mydomain.com?param1=test will NOT work...
96 | // this will redirect to mydomain.com?param1=test#/timeline
97 | // WHICH IS RETARDED, VUE
98 | // anyway... if we are in this situation, manually copy the parameters over
99 | // and use them in the redirect so stuff works
100 | router.beforeEach((to, from, next) => {
101 |
102 | if ( window.location.search && Object.keys(to.query).length === 0 && from.path === "/" ){
103 | const params = new URLSearchParams(window.location.search);
104 | const query:any = {};
105 | for ( const entry of params.entries() ){
106 | query[ entry[0] ] = entry[1];
107 | }
108 | next({ name: to.name, query: query });
109 | }
110 | else if( !hasQueryParams(to) && hasQueryParams(from) ){
111 | // generally allow passing query parameters between navigations (otherwise they get lost)
112 | // e.g., see https://stackoverflow.com/questions/45091380/vue-router-keep-query-parameter-and-use-same-view-for-children
113 | next({name: to.name, query: from.query});
114 | }
115 | else {
116 | next()
117 | }
118 | });
119 |
120 | export default router;
121 |
--------------------------------------------------------------------------------
/visualizations/src/store.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 |
4 | import ConnectionStore from "@/store/ConnectionStore"
5 | import ConfigurationStore from "@/store/ConfigurationStore"
6 |
7 | Vue.use(Vuex);
8 |
9 | export default new Vuex.Store({
10 | //strict: process.env.NODE_ENV !== 'production',
11 | modules: {
12 | connections: ConnectionStore,
13 | configurations: ConfigurationStore,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/visualizations/src/store/ConfigurationStore.ts:
--------------------------------------------------------------------------------
1 | import {VuexModule, Module, Mutation, Action} from 'vuex-module-decorators'
2 | import SequenceDiagramConfig from "@/components/sequencediagram/data/SequenceDiagramConfig";
3 | import CongestionGraphConfig from '@/components/congestiongraph/data/CongestionGraphConfig';
4 | import StatisticsConfig from '@/components/stats/data/StatisticsConfig';
5 | import MultiplexingGraphConfig from '@/components/multiplexinggraph/data/MultiplexingGraphConfig';
6 | import PacketizationDiagramConfig from '@/components/packetizationdiagram/data/PacketizationDiagramConfig';
7 |
8 | @Module({name: 'configurations'})
9 | export default class ConfigurationStore extends VuexModule {
10 |
11 | public congestionGraphConfig: CongestionGraphConfig = new CongestionGraphConfig();
12 | public sequenceDiagramConfig: SequenceDiagramConfig = new SequenceDiagramConfig();
13 | public statisticsConfig: StatisticsConfig = new StatisticsConfig();
14 | public multiplexingGraphConfig: MultiplexingGraphConfig = new MultiplexingGraphConfig();
15 | public packetizationDiagramConfig: PacketizationDiagramConfig = new PacketizationDiagramConfig();
16 | }
17 |
--------------------------------------------------------------------------------
/visualizations/src/types/oboe.d.ts:
--------------------------------------------------------------------------------
1 | declare module "oboe"; // because TypeScript cannot figure this out for itself... DX indeed
--------------------------------------------------------------------------------
/visualizations/src/types/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue';
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/visualizations/src/types/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue';
3 | export default Vue;
4 | }
5 |
--------------------------------------------------------------------------------
/visualizations/src/views/CongestionGraph.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/views/FileManager.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/views/MainMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
22 |
23 | qvis
24 |
25 |
26 |
27 | Manage files
28 |
29 |
30 | Sequence
31 |
32 |
33 | Congestion
34 |
35 |
36 | Multiplexing
37 |
38 |
39 | Packetization
40 |
41 |
42 | qlog stats
43 |
44 |
45 |
46 |
47 | Request feature
48 | Report issue
49 |
50 |
51 |
52 |
53 |
54 |
132 |
133 |
--------------------------------------------------------------------------------
/visualizations/src/views/MultiplexingGraph.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/views/PacketizationDiagram.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/views/SequenceDiagram.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/views/Statistics.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/visualizations/src/views/VUEDebug.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Add new random ConnectionGroup |
6 |
Delete First |
7 |
Change Connection Name |
8 |
Change Event Name |
9 |
RemoveEvent
10 |
11 |
12 | {{ connectionGroup.description }}
13 |
14 |
15 |
16 | - Event: ROBIN : {{ connection.title }} : {{ connection.parseEvent(event).name }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
89 |
--------------------------------------------------------------------------------
/visualizations/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "sourceMap": true,
13 | "baseUrl": ".",
14 | "types": [
15 | "webpack-env"
16 | ],
17 | "paths": {
18 | "@/*": [
19 | "src/*"
20 | ]
21 | },
22 | "lib": [
23 | "esnext",
24 | "dom",
25 | "dom.iterable",
26 | "scripthost"
27 | ]
28 | },
29 | "include": [
30 | "src/**/*.ts",
31 | "src/**/*.tsx",
32 | "src/**/*.vue",
33 | "tests/**/*.ts",
34 | "tests/**/*.tsx"
35 | ],
36 | "exclude": [
37 | "node_modules"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/visualizations/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warning",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "linterOptions": {
7 | "exclude": [
8 | "node_modules/**"
9 | ]
10 | },
11 | "rules": {
12 | "quotemark": false,
13 | "indent": [true, "spaces", 4],
14 | "array-type": [true, "generic"],
15 | "interface-name": false,
16 | "one-line": false,
17 | "ordered-imports": false,
18 | "max-line-length": [true, 250],
19 | "no-console": false,
20 | "object-literal-shorthand": false,
21 | "no-unnecessary-initializer": false,
22 | "object-literal-sort-keys": false,
23 | "no-consecutive-blank-lines": false,
24 | "typedef-whitespace": false,
25 | "no-trailing-whitespace": false,
26 | "semicolon": false,
27 | "no-import-side-effect": false,
28 | "no-parameter-reassignment": true,
29 | "arrow-return-shorthand": false,
30 | "prefer-for-of": true,
31 | "await-promise": true,
32 | "curly": true,
33 | "no-floating-promises": false,
34 | "switch-default" : true,
35 | "eofline": true,
36 | "arrow-parens": true,
37 | "newline-before-return": true,
38 | "whitespace": [true, "check-decl", "check-branch", "check-operator", "check-typecast"],
39 | "variable-name": [true, "allow-leading-underscore", "ban-keywords"]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/visualizations/vue.config.js:
--------------------------------------------------------------------------------
1 | // get rid of absolute paths in the .html output
2 | // https://github.com/vuejs/vue-cli/issues/1623
3 |
4 | module.exports = {
5 | baseUrl: './',
6 | configureWebpack: {
7 | devtool: 'source-map'
8 | }
9 | }
10 |
--------------------------------------------------------------------------------