From ae6a840eb5d3e4f64a2d68b6f4a7fc00997d37cd Mon Sep 17 00:00:00 2001 From: Jeremy Rifkin <51220084+jeremy-rifkin@users.noreply.github.com> Date: Sun, 18 Dec 2022 10:30:17 -0500 Subject: [PATCH] Assert and unwrap utilities (#4437) * Add assert and unwrap utilities * Playing around with diagnostics * lib/assert diagnostic implementation * Remove temporary testing stuff * Reset package-lock.json to before I messed with it * Further refinements and integration * Added licence and removed an obsolete eslint directive --- .eslintrc.yml | 3 + app.js | 3 + lib/assert.ts | 98 +++++++++++++++ lib/compilers/carbon.ts | 3 +- lib/parsers/llvm-pass-dump-parser.ts | 50 ++------ lib/stacktrace.ts | 177 +++++++++++++++++++++++++++ static/.eslint-ce-static.yml | 3 + static/assert.ts | 80 ++++++++++++ static/panes/cfg-view.ts | 34 +++-- 9 files changed, 391 insertions(+), 60 deletions(-) create mode 100644 lib/assert.ts create mode 100644 lib/stacktrace.ts create mode 100644 static/assert.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 051465483de..a62820f4dd2 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -74,6 +74,9 @@ rules: no-useless-escape: error no-useless-rename: error no-useless-return: error + no-empty: + - error + - allowEmptyCatch: true quote-props: - error - as-needed diff --git a/app.js b/app.js index 3da2d23ade5..04462b6cced 100755 --- a/app.js +++ b/app.js @@ -67,6 +67,9 @@ import {loadSponsorsFromString} from './lib/sponsors'; import {getStorageTypeByKey} from './lib/storage'; import * as utils from './lib/utils'; +// Used by assert.ts +global.ce_base_directory = __dirname; // eslint-disable-line unicorn/prefer-module + // Parse arguments from command line 'node ./app.js args...' const opts = nopt({ env: [String, Array], diff --git a/lib/assert.ts b/lib/assert.ts new file mode 100644 index 00000000000..e07b8d374a6 --- /dev/null +++ b/lib/assert.ts @@ -0,0 +1,98 @@ +// Copyright (c) 2022, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +import * as fs from 'fs'; +import path from 'path'; + +import stacktrace from './stacktrace'; + +function check_path(parent: string, directory: string) { + // https://stackoverflow.com/a/45242825/15675011 + const relative = path.relative(parent, directory); + if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) { + return relative; + } else { + return false; + } +} + +function get_diagnostic() { + const e = new Error(); // eslint-disable-line unicorn/error-message + const trace = stacktrace.parse(e); + if (trace.length >= 4) { + const invoker_frame = trace[3]; + if (invoker_frame.fileName && invoker_frame.lineNumber) { + // Just out of an abundance of caution... + const relative = check_path(global.ce_base_directory, invoker_frame.fileName); + if (relative) { + try { + const file = fs.readFileSync(invoker_frame.fileName, 'utf-8'); + const lines = file.split('\n'); + return { + file: relative, + line: invoker_frame.lineNumber, + src: lines[invoker_frame.lineNumber - 1].trim(), + }; + } catch (e: any) {} + } + } + } +} + +function fail(fail_message: string, user_message: string | undefined, args: any[]): never { + // Assertions will look like: + // Assertion failed + // Assertion failed: Foobar + // Assertion failed: Foobar, [{"foo": "bar"}] + // Assertion failed: Foobar, [{"foo": "bar"}], at `assert(x.foo.length < 2, "Foobar", x)` + let assert_line = fail_message; + if (user_message) { + assert_line += `: ${user_message}`; + } + if (args.length > 0) { + try { + assert_line += ', ' + JSON.stringify(args); + } catch (e) {} + } + + const diagnostic = get_diagnostic(); + if (diagnostic) { + throw new Error(assert_line + `, at ${diagnostic.file}:${diagnostic.line} \`${diagnostic.src}\``); + } else { + throw new Error(assert_line); + } +} + +export function assert(c: C, message?: string, ...extra_info: any[]): asserts c { + if (!c) { + fail('Assertion failed', message, extra_info); + } +} + +export function unwrap(x: T | undefined | null, message?: string, ...extra_info: any[]): T { + if (x === undefined || x === null) { + fail('Unwrap failed', message, extra_info); + } + return x; +} diff --git a/lib/compilers/carbon.ts b/lib/compilers/carbon.ts index 88131e801d1..68d9da5eaa4 100644 --- a/lib/compilers/carbon.ts +++ b/lib/compilers/carbon.ts @@ -27,6 +27,7 @@ import {CompilationResult} from '../../types/compilation/compilation.interfaces' import {CompilerInfo} from '../../types/compiler.interfaces'; import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces'; import {ResultLine} from '../../types/resultline/resultline.interfaces'; +import {unwrap} from '../assert'; import {BaseCompiler} from '../base-compiler'; import {BaseParser} from './argument-parsers'; @@ -69,7 +70,7 @@ export class CarbonCompiler extends BaseCompiler { lastLine(lines?: ResultLine[]): string { if (!lines || lines.length === 0) return ''; - return (lines.at(-1) as ResultLine).text; + return unwrap(lines.at(-1)).text; } override postCompilationPreCacheHook(result: CompilationResult): CompilationResult { diff --git a/lib/parsers/llvm-pass-dump-parser.ts b/lib/parsers/llvm-pass-dump-parser.ts index a210dbabba5..fc8fe209957 100644 --- a/lib/parsers/llvm-pass-dump-parser.ts +++ b/lib/parsers/llvm-pass-dump-parser.ts @@ -31,6 +31,7 @@ import { } from '../../types/compilation/llvm-opt-pipeline-output.interfaces'; import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces'; import {ResultLine} from '../../types/resultline/resultline.interfaces'; +import {assert} from '../assert'; // Note(jeremy-rifkin): // For now this filters out a bunch of metadata we aren't interested in @@ -38,20 +39,6 @@ import {ResultLine} from '../../types/resultline/resultline.interfaces'; // It'd be helpful to better display annotations about branch weights // and parse debug info too at some point. -// TODO(jeremy-rifkin): Doe we already have an assert utility -function assert(condition: boolean, message?: string, ...args: any[]): asserts condition { - if (!condition) { - const stack = new Error('Assertion Error').stack; - throw ( - (message - ? `Assertion error in llvm-print-after-all-parser: ${message}` - : `Assertion error in llvm-print-after-all-parser`) + - (args.length > 0 ? `\n${JSON.stringify(args)}\n` : '') + - `\n${stack}` - ); - } -} - // Just a sanity check function passesMatch(before: string, after: string) { assert(before.startsWith('IR Dump Before ')); @@ -175,9 +162,7 @@ export class LlvmPassDumpParser { }; lastWasBlank = true; // skip leading newlines after the header } else { - if (pass === null) { - throw 'Internal error during breakdownOutput (1)'; - } + assert(pass); if (line.text.trim() === '') { if (!lastWasBlank) { pass.lines.push(line); @@ -216,9 +201,7 @@ export class LlvmPassDumpParser { // function define line if (irFnMatch || machineFnMatch) { // if the last function has not been closed... - if (func !== null) { - throw 'Internal error during breakdownPass (1)'; - } + assert(func === null); func = { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion name: (irFnMatch || machineFnMatch)![1], @@ -236,19 +219,13 @@ export class LlvmPassDumpParser { // close function if (this.functionEnd.test(line.text.trim())) { // if not currently in a function - if (func === null) { - throw 'Internal error during breakdownPass (2)'; - } + assert(func); const {name, lines} = func; lines.push(line); // include the } // loop dumps can't be terminated with } - if (name === '') { - throw 'Internal error during breakdownPass (3)'; - } + assert(name !== ''); // somehow dumped twice? - if (name in pass.functions) { - throw 'Internal error during breakdownPass (4)'; - } + assert(!(name in pass.functions)); pass.functions[name] = lines; func = null; } else { @@ -269,17 +246,10 @@ export class LlvmPassDumpParser { } // unterminated function, either a loop dump or an error if (func !== null) { - if (func.name === '') { - // loop dumps must be alone - if (Object.entries(pass.functions).length > 0) { - //console.dir(dump, { depth: 5, maxArrayLength: 100000 }); - //console.log(pass.functions); - throw 'Internal error during breakdownPass (5)'; - } - pass.functions[func.name] = func.lines; - } else { - throw 'Internal error during breakdownPass (6)'; - } + assert(func.name === ''); + // loop dumps must be alone + assert(Object.entries(pass.functions).length === 0); + pass.functions[func.name] = func.lines; } return pass; } diff --git a/lib/stacktrace.ts b/lib/stacktrace.ts new file mode 100644 index 00000000000..68320111c90 --- /dev/null +++ b/lib/stacktrace.ts @@ -0,0 +1,177 @@ +// Copyright (c) 2022, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +// Based on stack-trace https://github.com/felixge/node-stack-trace + +type StackFrame = { + fileName: string | undefined; + lineNumber?: number; + functionName?: string; + typeName?: string; + methodName?: string; + columnNumber?: number; + native?: boolean; +}; + +enum TraceFormat { + V8, + Firefox, +} + +function parse(err: Error) { + if (!err.stack) { + return []; + } + + let format: TraceFormat; + + if (typeof window === 'undefined') { + // node + format = TraceFormat.V8; + } else { + if (navigator.userAgent.includes('AppleWebKit')) { + // Just going with V8 for now... + format = TraceFormat.V8; + } else if ((window as any).chrome) { + format = TraceFormat.V8; + } else if (navigator.userAgent.toLowerCase().includes('firefox')) { + format = TraceFormat.Firefox; + } else { + // We'll just default to V8 for now... + format = TraceFormat.V8; + } + } + if (format === TraceFormat.V8) { + return err.stack + .split('\n') + .slice(1) + .map((line): StackFrame | undefined => { + if (/^\s*-{4,}$/.test(line)) { + return { + fileName: line, + }; + } + + const lineMatch = line.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/); + if (!lineMatch) { + return; + } + + let object: string | undefined; + let method: string | undefined; + let functionName: string | undefined; + let typeName: string | undefined; + let methodName: string | undefined; + const isNative = lineMatch[5] === 'native'; + + if (lineMatch[1]) { + functionName = lineMatch[1]; + let methodStart = functionName.lastIndexOf('.'); + if (functionName[methodStart - 1] === '.') methodStart--; + if (methodStart > 0) { + object = functionName.substring(0, methodStart); + method = functionName.substring(methodStart + 1); + const objectEnd = object.indexOf('.Module'); + if (objectEnd > 0) { + functionName = functionName.substring(objectEnd + 1); + object = object.substring(0, objectEnd); + } + } + } + + if (method) { + typeName = object; + methodName = method; + } + + if (method === '') { + methodName = undefined; + functionName = undefined; + } + + return { + fileName: lineMatch[2] || undefined, + lineNumber: parseInt(lineMatch[3], 10) || undefined, + functionName: functionName, + typeName: typeName, + methodName: methodName, + columnNumber: parseInt(lineMatch[4], 10) || undefined, + native: isNative, + }; + }) + .filter(frame => frame !== undefined) as StackFrame[]; + } else { + return err.stack + .split('\n') + .map((line): StackFrame | undefined => { + const lineMatch = line.match(/(.*)@(.*):(\d+):(\d+)/); + if (!lineMatch) { + return; + } + + let object: string | undefined; + let method: string | undefined; + let functionName: string | undefined; + let typeName: string | undefined; + let methodName: string | undefined; + + if (lineMatch[1]) { + functionName = lineMatch[1]; + let methodStart = functionName.lastIndexOf('.'); + if (functionName[methodStart - 1] === '.') methodStart--; + if (methodStart > 0) { + object = functionName.substring(0, methodStart); + method = functionName.substring(methodStart + 1); + const objectEnd = object.indexOf('.Module'); + if (objectEnd > 0) { + functionName = functionName.substring(objectEnd + 1); + object = object.substring(0, objectEnd); + } + } + } + + if (method) { + typeName = object; + methodName = method; + } + + if (method === '') { + methodName = undefined; + functionName = undefined; + } + + return { + fileName: lineMatch[2] || undefined, + lineNumber: parseInt(lineMatch[3], 10) || undefined, + functionName: functionName, + typeName: typeName, + methodName: methodName, + columnNumber: parseInt(lineMatch[4], 10) || undefined, + }; + }) + .filter(frame => frame !== undefined) as StackFrame[]; + } +} + +export default {parse}; // eslint-disable-line import/no-default-export diff --git a/static/.eslint-ce-static.yml b/static/.eslint-ce-static.yml index ccf4ae15a31..c4177f6d369 100644 --- a/static/.eslint-ce-static.yml +++ b/static/.eslint-ce-static.yml @@ -44,6 +44,9 @@ rules: no-useless-escape: error no-useless-rename: error no-useless-return: error + no-empty: + - error + - allowEmptyCatch: true quote-props: - error - as-needed diff --git a/static/assert.ts b/static/assert.ts new file mode 100644 index 00000000000..4c704a8275a --- /dev/null +++ b/static/assert.ts @@ -0,0 +1,80 @@ +// Copyright (c) 2022, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +import stacktrace from '../lib/stacktrace'; + +// This file defines three assert utilities: +// assert(condition, message?, extra_info...?): asserts condition +// unwrap(x: T | undefined | null, message?, extra_info...?): T +// assert_type(x, class, message?, extra_info...?) + +function get_diagnostic() { + const e = new Error(); // eslint-disable-line unicorn/error-message + const trace = stacktrace.parse(e); + if (trace.length >= 4) { + const invoker_frame = trace[3]; + if (invoker_frame.fileName && invoker_frame.lineNumber) { + return { + file: invoker_frame.fileName, + line: invoker_frame.lineNumber, + }; + } + } +} + +function fail(fail_message: string, user_message: string | undefined, args: any[]): never { + // Assertions will look like: + // Assertion failed + // Assertion failed: Foobar + // Assertion failed: Foobar, [{"foo": "bar"}] + let assert_line = fail_message; + if (user_message) { + assert_line += `: ${user_message}`; + } + if (args.length > 0) { + try { + assert_line += ', ' + JSON.stringify(args); + } catch (e) {} + } + + const diagnostic = get_diagnostic(); + if (diagnostic) { + throw new Error(assert_line + `, at ${diagnostic.file}:${diagnostic.line}`); + } else { + throw new Error(assert_line); + } +} + +export function assert(c: C, message?: string, ...extra_info: any[]): asserts c { + if (!c) { + fail('Assertion failed', message, extra_info); + } +} + +export function unwrap(x: T | undefined | null, message?: string, ...extra_info: any[]): T { + if (x === undefined || x === null) { + fail('Unwrap failed', message, extra_info); + } + return x; +} diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts index f115ca4e9f1..cd2e4baafcc 100644 --- a/static/panes/cfg-view.ts +++ b/static/panes/cfg-view.ts @@ -44,6 +44,7 @@ import { import {GraphLayoutCore} from '../graph-layout-core'; import * as MonacoConfig from '../monaco-config'; import TomSelect from 'tom-select'; +import {assert, unwrap} from '../assert'; const ColorTable = { red: '#FE5D5D', @@ -113,9 +114,7 @@ export class Cfg extends Pane { this.eventHub.emit('requestFilters', this.compilerInfo.compilerId); this.eventHub.emit('requestCompiler', this.compilerInfo.compilerId); const selector = this.domRoot.get()[0].getElementsByClassName('function-selector')[0]; - if (!(selector instanceof HTMLSelectElement)) { - throw new Error('.function-selector is not an HTMLSelectElement'); - } + assert(selector instanceof HTMLSelectElement, '.function-selector is not an HTMLSelectElement'); this.functionSelector = new TomSelect(selector, { valueField: 'value', labelField: 'title', @@ -125,7 +124,7 @@ export class Cfg extends Pane { plugins: ['dropdown_input'], sortField: 'title', onChange: e => { - this.selectFunction(e as any as string); + this.selectFunction(e as string); }, }); this.state = state; @@ -152,7 +151,7 @@ export class Cfg extends Pane { override registerDynamicElements(state: CfgState) { this.graphDiv = this.domRoot.find('.graph')[0]; - this.svg = this.domRoot.find('svg')[0] as SVGElement; + this.svg = this.domRoot.find('svg')[0]; this.blockContainer = this.domRoot.find('.block-container')[0]; this.graphContainer = this.domRoot.find('.graph-container')[0]; this.graphElement = this.domRoot.find('.graph')[0]; @@ -163,7 +162,7 @@ export class Cfg extends Pane { override registerCallbacks() { this.graphContainer.addEventListener('mousedown', e => { - const div = (e.target as Element).closest('div'); + const div = (unwrap(e.target) as Element).closest('div'); if (div && (div.classList.contains('block-container') || div.classList.contains('graph-container'))) { this.dragging = true; this.dragStart = {x: e.clientX, y: e.clientY}; @@ -274,17 +273,18 @@ export class Cfg extends Pane { const div = document.createElement('div'); div.classList.add('block'); div.innerHTML = await monaco.editor.colorize(node.label, 'asm', MonacoConfig.extendConfig({})); - if (node.id in this.bbMap) { - throw Error("Duplicate basic block node id's found while drawing cfg"); - } + // So because this is async there's a race condition here if you rapidly switch functions. + // This can be triggered by loading an example program. Because the fix going to be tricky I'll defer + // to another PR. TODO(jeremy-rifkin) + assert(!(node.id in this.bbMap), "Duplicate basic block node id's found while drawing cfg"); this.bbMap[node.id] = div; this.blockContainer.appendChild(div); } for (const node of fn.nodes) { const elem = $(this.bbMap[node.id]); void this.bbMap[node.id].offsetHeight; - (node as AnnotatedNodeDescriptor).width = elem.outerWidth() as number; - (node as AnnotatedNodeDescriptor).height = elem.outerHeight() as number; + (node as AnnotatedNodeDescriptor).width = unwrap(elem.outerWidth()); + (node as AnnotatedNodeDescriptor).height = unwrap(elem.outerHeight()); } } @@ -299,9 +299,7 @@ export class Cfg extends Pane { for (const block of this.layout.blocks) { for (const edge of block.edges) { // Sanity check - if (edge.path.length === 0) { - throw Error('Mal-formed edge: Zero segments'); - } + assert(edge.path.length !== 0, 'Mal-formed edge: Zero segments'); const points: [number, number][] = []; // -1 offset is to create an overlap between the block's bottom border and start of the path, avoid any // visual artifacts @@ -396,9 +394,7 @@ export class Cfg extends Pane { doc += ''; // insert the background const pane = this.graphContainer.parentElement; - if (!pane || !pane.classList.contains('lm_content')) { - throw Error('unknown parent'); - } + assert(pane && pane.classList.contains('lm_content'), 'Unknown parent'); const pane_style = window.getComputedStyle(pane); doc += ` { override resize() { _.defer(() => { const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable); - this.graphContainer.style.width = `${this.domRoot.width() as number}px`; - this.graphContainer.style.height = `${(this.domRoot.height() as number) - topBarHeight}px`; + this.graphContainer.style.width = `${unwrap(this.domRoot.width())}px`; + this.graphContainer.style.height = `${unwrap(this.domRoot.height()) - topBarHeight}px`; }); }