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>
Mode |
Size |
Compressed |
Name |
${zip_files.map(zf => html`
${{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`
Device |
Bytes |
Human |
${Object.entries(sizes).map(([dev, size]) => html`
${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);