import { h, Component, render } from 'https://unpkg.com/preact?module'; import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); const BURNED_IN_MODEL_INFO = null; // https://stackoverflow.com/a/20732091 function humanFileSize(size) { if (size == 0) { return "0 B"; } var i = Math.floor( Math.log(size) / Math.log(1024) ); return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; } function caret(down) { return down ? "\u25BE" : "\u25B8"; } class Blamer { constructor() { this.blame_on_click = false; this.aux_content_pane = null; } setAuxContentPane(pane) { this.aux_content_pane = pane; } readyBlame() { this.blame_on_click = true; } maybeBlame(arg) { if (!this.blame_on_click) { return; } this.blame_on_click = false; if (!this.aux_content_pane) { return; } this.aux_content_pane.doBlame(arg); } } let blame = new Blamer(); class Hider extends Component { constructor() { super(); this.state = { shown: null }; } componentDidMount() { this.setState({ shown: this.props.shown === "true" }); } render({name, children}, {shown}) { let my_caret = html` this.click()} >${caret(shown)}`; return html`

${my_caret} ${name}

${shown ? this.props.children : []}
`; } click() { this.setState({shown: !this.state.shown}); } } function ModelSizeSection({model: {file_size, zip_files}}) { let store_size = 0; let compr_size = 0; for (const zi of zip_files) { if (zi.compression === 0) { // TODO: Maybe check that compressed_size === file_size. store_size += zi.compressed_size; } else { compr_size += zi.compressed_size; } } let zip_overhead = file_size - store_size - compr_size; // TODO: Better formatting. Right-align this. return html` <${Hider} name="Model Size" shown=true>
.
      Model size: ${file_size} (${humanFileSize(file_size)})
      Stored files: ${store_size} (${humanFileSize(store_size)})
      Compressed files: ${compr_size} (${humanFileSize(compr_size)})
      Zip overhead: ${zip_overhead} (${humanFileSize(zip_overhead)})
    
`; } function StructuredDataSection({name, data, shown}) { return html` <${Hider} name=${name} shown=${shown}>
<${StructuredData} data=${data} indent="" prefix=""/>
`; } class StructuredData extends Component { constructor() { super(); this.state = { shown: false }; this.INLINE_TYPES = new Set(["boolean", "number", "string"]) this.IGNORED_STATE_KEYS = new Set(["training", "_is_full_backward_hook"]) } click() { this.setState({shown: !this.state.shown}); } expando(data) { if (data === null || this.INLINE_TYPES.has(typeof(data))) { return false; } if (typeof(data) != "object") { throw new Error("Not an object"); } if (Array.isArray(data)) { // TODO: Maybe show simple lists and tuples on one line. return true; } if (data.__tuple_values__) { // TODO: Maybe show simple lists and tuples on one line. return true; } if (data.__is_dict__) { // TODO: Maybe show simple (empty?) dicts on one line. return true; } if (data.__module_type__) { return true; } if (data.__tensor_v2__) { return false; } if (data.__qtensor__) { return false; } throw new Error("Can't handle data type.", data); } renderHeadline(data) { if (data === null) { return "None"; } if (typeof(data) == "boolean") { const sd = String(data); return sd.charAt(0).toUpperCase() + sd.slice(1); } if (typeof(data) == "number") { return JSON.stringify(data); } if (typeof(data) == "string") { return JSON.stringify(data); } if (typeof(data) != "object") { throw new Error("Not an object"); } if (Array.isArray(data)) { return "list(["; } if (data.__tuple_values__) { return "tuple(("; } if (data.__is_dict__) { return "dict({"; } if (data.__module_type__) { return data.__module_type__ + "()"; } if (data.__tensor_v2__) { const [storage, offset, size, stride, grad] = data.__tensor_v2__; const [dtype, key, device, numel] = storage; return this.renderTensor( "tensor", dtype, key, device, numel, offset, size, stride, grad, []); } if (data.__qtensor__) { const [storage, offset, size, stride, quantizer, grad] = data.__qtensor__; const [dtype, key, device, numel] = storage; let extra_parts = []; if (quantizer[0] == "per_tensor_affine") { extra_parts.push(`scale=${quantizer[1]}`); extra_parts.push(`zero_point=${quantizer[2]}`); } else { extra_parts.push(`quantizer=${quantizer[0]}`); } return this.renderTensor( "qtensor", dtype, key, device, numel, offset, size, stride, grad, extra_parts); } throw new Error("Can't handle data type.", data); } renderTensor( prefix, dtype, storage_key, device, storage_numel, offset, size, stride, grad, extra_parts) { let parts = [ "(" + size.join(",") + ")", dtype, ]; parts.push(...extra_parts); if (device != "cpu") { parts.push(device); } if (grad) { parts.push("grad"); } // TODO: Check stride and indicate if the tensor is channels-last or non-contiguous // TODO: Check size, stride, offset, and numel and indicate if // the tensor doesn't use all data in storage. // TODO: Maybe show key? void(offset); void(stride); void(storage_key); void(storage_numel); return prefix + "(" + parts.join(", ") + ")"; } renderBody(indent, data) { if (data === null || this.INLINE_TYPES.has(typeof(data))) { throw "Should not reach here." } if (typeof(data) != "object") { throw new Error("Not an object"); } if (Array.isArray(data)) { let new_indent = indent + "\u00A0\u00A0"; let parts = []; for (let idx = 0; idx < data.length; idx++) { // Does it make sense to put explicit index numbers here? parts.push(html`
<${StructuredData} prefix=${idx + ": "} indent=${new_indent} data=${data[idx]} />`); } return parts; } if (data.__tuple_values__) { // Handled the same as lists. return this.renderBody(indent, data.__tuple_values__); } if (data.__is_dict__) { let new_indent = indent + "\u00A0\u00A0"; let parts = []; for (let idx = 0; idx < data.keys.length; idx++) { if (typeof(data.keys[idx]) != "string") { parts.push(html`
${new_indent}Non-string key`); } else { parts.push(html`
<${StructuredData} prefix=${data.keys[idx] + ": "} indent=${new_indent} data=${data.values[idx]} />`); } } return parts; } if (data.__module_type__) { const mstate = data.state; if (mstate === null || typeof(mstate) != "object") { throw new Error("Bad module state"); } let new_indent = indent + "\u00A0\u00A0"; let parts = []; if (mstate.__is_dict__) { // TODO: Less copy/paste between this and normal dicts. for (let idx = 0; idx < mstate.keys.length; idx++) { if (typeof(mstate.keys[idx]) != "string") { parts.push(html`
${new_indent}Non-string key`); } else if (this.IGNORED_STATE_KEYS.has(mstate.keys[idx])) { // Do nothing. } else { parts.push(html`
<${StructuredData} prefix=${mstate.keys[idx] + ": "} indent=${new_indent} data=${mstate.values[idx]} />`); } } } else if (mstate.__tuple_values__) { parts.push(html`
<${StructuredData} prefix="" indent=${new_indent} data=${mstate} />`); } else if (mstate.__module_type__) { // We normally wouldn't have the state of a module be another module, // but we use "modules" to encode special values (like Unicode decode // errors) that might be valid states. Just go with it. parts.push(html`
<${StructuredData} prefix="" indent=${new_indent} data=${mstate} />`); } else { throw new Error("Bad module state"); } return parts; } if (data.__tensor_v2__) { throw "Should not reach here." } if (data.__qtensor__) { throw "Should not reach here." } throw new Error("Can't handle data type.", data); } render({data, indent, prefix}, {shown}) { const exp = this.expando(data) ? html` this.click()} >${caret(shown)} ` : ""; const headline = this.renderHeadline(data); const body = shown ? this.renderBody(indent, data) : ""; return html`${indent}${exp}${prefix}${headline}${body}`; } } function ZipContentsSection({model: {zip_files}}) { // TODO: Add human-readable sizes? // TODO: Add sorting options? // TODO: Add hierarchical collapsible tree? return html` <${Hider} name="Zip Contents" shown=false> ${zip_files.map(zf => html``)}
Mode Size Compressed Name
${{0: "store", 8: "deflate"}[zf.compression] || zf.compression} ${zf.file_size} ${zf.compressed_size} ${zf.filename}
`; } function CodeSection({model: {code_files}}) { return html` <${Hider} name="Code" shown=false>
${Object.entries(code_files).map(([fn, code]) => html`<${OneCodeSection} filename=${fn} code=${code} />`)}
`; } class OneCodeSection extends Component { constructor() { super(); this.state = { shown: false }; } click() { const shown = !this.state.shown; this.setState({shown: shown}); } render({filename, code}, {shown}) { const header = html`

this.click()} >${caret(shown)} ${filename}

`; if (!shown) { return header; } return html` ${header}
${code.map(c => this.renderBlock(c))}
`; } renderBlock([text, ist_file, line, ist_s_text, s_start, s_end]) { return html` blame.maybeBlame({ist_file, line, ist_s_text, s_start, s_end})} >${text}`; } } function ExtraJsonSection({files}) { return html` <${Hider} name="Extra files (JSON)" shown=false>

Use "Log Raw Model Info" for hierarchical view in browser console.

${Object.entries(files).map(([fn, json]) => html`<${OneJsonSection} filename=${fn} json=${json} />`)}
`; } class OneJsonSection extends Component { constructor() { super(); this.state = { shown: false }; } click() { const shown = !this.state.shown; this.setState({shown: shown}); } render({filename, json}, {shown}) { const header = html`

this.click()} >${caret(shown)} ${filename}

`; if (!shown) { return header; } return html` ${header}
${JSON.stringify(json, null, 2)}
`; } } function ExtraPicklesSection({files}) { return html` <${Hider} name="Extra Pickles" shown=false>
${Object.entries(files).map(([fn, content]) => html`<${OnePickleSection} filename=${fn} content=${content} />`)}
`; } class OnePickleSection extends Component { constructor() { super(); this.state = { shown: false }; } click() { const shown = !this.state.shown; this.setState({shown: shown}); } render({filename, content}, {shown}) { const header = html`

this.click()} >${caret(shown)} ${filename}

`; if (!shown) { return header; } return html` ${header}
${content}
`; } } function assertStorageAreEqual(key, lhs, rhs) { if (lhs.length !== rhs.length || !lhs.every((val, idx) => val === rhs[idx])) { throw new Error("Storage mismatch for key '" + key + "'"); } } function computeTensorMemory(numel, dtype) { const sizes = { "Byte": 1, "Char": 1, "Short": 2, "Int": 4, "Long": 8, "Half": 2, "Float": 4, "Double": 8, "ComplexHalf": 4, "ComplexFloat": 8, "ComplexDouble": 16, "Bool": 1, "QInt8": 1, "QUInt8": 1, "QInt32": 4, "BFloat16": 2, }; let dtsize = sizes[dtype]; if (!dtsize) { throw new Error("Unrecognized dtype: " + dtype); } return numel * dtsize; } // TODO: Maybe track by dtype as well. // TODO: Maybe distinguish between visible size and storage size. function getTensorStorages(data) { if (data === null) { return new Map(); } if (typeof(data) == "boolean") { return new Map(); } if (typeof(data) == "number") { return new Map(); } if (typeof(data) == "string") { return new Map(); } if (typeof(data) != "object") { throw new Error("Not an object"); } if (Array.isArray(data)) { let result = new Map(); for (const item of data) { const tensors = getTensorStorages(item); for (const [key, storage] of tensors.entries()) { if (!result.has(key)) { result.set(key, storage); } else { const old_storage = result.get(key); assertStorageAreEqual(key, old_storage, storage); } } } return result; } if (data.__tuple_values__) { return getTensorStorages(data.__tuple_values__); } if (data.__is_dict__) { return getTensorStorages(data.values); } if (data.__module_type__) { return getTensorStorages(data.state); } if (data.__tensor_v2__) { const [storage, offset, size, stride, grad] = data.__tensor_v2__; const [dtype, key, device, numel] = storage; return new Map([[key, storage]]); } if (data.__qtensor__) { const [storage, offset, size, stride, quantizer, grad] = data.__qtensor__; const [dtype, key, device, numel] = storage; return new Map([[key, storage]]); } throw new Error("Can't handle data type.", data); } function getTensorMemoryByDevice(pickles) { let all_tensors = []; for (const [name, pickle] of pickles) { const tensors = getTensorStorages(pickle); all_tensors.push(...tensors.values()); } let result = {}; for (const storage of all_tensors.values()) { const [dtype, key, device, numel] = storage; const size = computeTensorMemory(numel, dtype); result[device] = (result[device] || 0) + size; } return result; } // Make this a separate component so it is rendered lazily. class OpenTensorMemorySection extends Component { render({model: {model_data, constants}}) { let sizes = getTensorMemoryByDevice(new Map([ ["data", model_data], ["constants", constants], ])); return html` ${Object.entries(sizes).map(([dev, size]) => html``)}
Device Bytes Human
${dev} ${size} ${humanFileSize(size)}
`; } } function TensorMemorySection({model}) { return html` <${Hider} name="Tensor Memory" shown=false> <${OpenTensorMemorySection} model=${model} />`; } class AuxContentPane extends Component { constructor() { super(); this.state = { blame_info: null, }; } doBlame(arg) { this.setState({...this.state, blame_info: arg}); } render({model: {interned_strings}}, {blame_info}) { let blame_content = ""; if (blame_info) { const {ist_file, line, ist_s_text, s_start, s_end} = blame_info; let s_text = interned_strings[ist_s_text]; if (s_start != 0 || s_end != s_text.length) { let prefix = s_text.slice(0, s_start); let main = s_text.slice(s_start, s_end); let suffix = s_text.slice(s_end); s_text = html`${prefix}${main}${suffix}`; } blame_content = html`

${interned_strings[ist_file]}:${line}

${s_start}:${s_end}
${s_text}

`; } return html`
${blame_content} `; } } class App extends Component { constructor() { super(); this.state = { err: false, model: null, }; } componentDidMount() { const app = this; if (BURNED_IN_MODEL_INFO !== null) { app.setState({model: BURNED_IN_MODEL_INFO}); } else { fetch("./model_info.json").then(function(response) { if (!response.ok) { throw new Error("Response not ok."); } return response.json(); }).then(function(body) { app.setState({model: body}); }).catch(function(error) { console.log("Top-level error: ", error); }); } } componentDidCatch(error) { void(error); this.setState({...this.state, err: true}); } render(_, {err}) { if (this.state.model === null) { return html`

Loading...

`; } const model = this.state.model.model; let error_msg = ""; if (err) { error_msg = html`

An error occurred. Check console

`; } return html` ${error_msg}

TorchScript Model (version ${model.version}): ${model.title}

<${ModelSizeSection} model=${model}/> <${StructuredDataSection} name="Model Data" data=${model.model_data} shown=true/> <${StructuredDataSection} name="Constants" data=${model.constants} shown=false/> <${ZipContentsSection} model=${model}/> <${CodeSection} model=${model}/> <${ExtraJsonSection} files=${model.extra_files_jsons}/> <${ExtraPicklesSection} files=${model.extra_pickles}/> <${TensorMemorySection} model=${model}/>
<${AuxContentPane} err=${this.state.error} model=${model} ref=${(p) => blame.setAuxContentPane(p)}/>
`; } } render(h(App), document.body);