├── .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 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /visualizations/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | 35 | 36 | 60 | -------------------------------------------------------------------------------- /visualizations/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /visualizations/src/components/congestiongraph/CongestionGraphConfigurator.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 38 | 39 | 141 | -------------------------------------------------------------------------------- /visualizations/src/components/congestiongraph/CongestionGraphContainer.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /visualizations/src/components/congestiongraph/CongestionGraphRenderer.vue: -------------------------------------------------------------------------------- 1 | 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 | 67 | 68 | 196 | -------------------------------------------------------------------------------- /visualizations/src/components/multiplexinggraph/MultiplexingGraphConfigurator.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 50 | 51 | 128 | -------------------------------------------------------------------------------- /visualizations/src/components/multiplexinggraph/MultiplexingGraphContainer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | -------------------------------------------------------------------------------- /visualizations/src/components/multiplexinggraph/MultiplexingGraphRenderer.vue: -------------------------------------------------------------------------------- 1 | 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 | 99 | 100 | 109 | 110 | 190 | -------------------------------------------------------------------------------- /visualizations/src/components/packetizationdiagram/PacketizationDiagramContainer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | -------------------------------------------------------------------------------- /visualizations/src/components/packetizationdiagram/PacketizationDiagramRenderer.vue: -------------------------------------------------------------------------------- 1 | 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 | 8 | 9 | 47 | -------------------------------------------------------------------------------- /visualizations/src/components/sequencediagram/SequenceDiagramRenderer.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /visualizations/src/components/sequencediagram/SequenceDiagramSimpleRenderer.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 18 | 19 | 28 | 29 | 77 | -------------------------------------------------------------------------------- /visualizations/src/components/stats/StatisticsContainer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | -------------------------------------------------------------------------------- /visualizations/src/components/stats/StatisticsRenderer.vue: -------------------------------------------------------------------------------- 1 | 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 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /visualizations/src/views/FileManager.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /visualizations/src/views/MainMenu.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 132 | 133 | -------------------------------------------------------------------------------- /visualizations/src/views/MultiplexingGraph.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /visualizations/src/views/PacketizationDiagram.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /visualizations/src/views/SequenceDiagram.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /visualizations/src/views/Statistics.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /visualizations/src/views/VUEDebug.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------