Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Storage-viz visualizers

  • Loading branch information...
commit 89d8b43c67a6ca53f6a3ab01ac8ca308f7bb2c5e 1 parent f623706
@utaal utaal authored
View
101 README.md
@@ -1,2 +1,99 @@
-mongo-storage-info
-==================
+Storage-viz, visualize Mongo's storage and indexes
+==================================================
+
+© 2012 10gen, the MongoDB Company
+
+Authors: Andrea Lattuada
+
+**The commands used by this tool are currently EXPERIMENTAL and UNSUPPORTED.**
+
+Storage-viz is a suite of web-based visualizers that leverage new database commands:
+they make it easier to understand and analyze MongoDB storage and btree layout.
+
+OVERVIEW
+--------
+
+The `storageDetails` command will aggregate statistics related to the
+storage layout (when invoked with `analyze: "diskStorage"`) or the percentage
+of pages currently in RAM (when invoked with `analyze: "pagesInRAM"`) for the
+specified collection, extent or part of extent.
+
+The `indexStats` command provides detailed and aggregate information and
+statistics for the underlying btree of a particular index.
+Stats are aggregated for the entire tree, per-depth and, if requested through
+the `expandNodes` option, per-subtree.
+
+Both commands take a global READ_LOCK and will page in all the extents or btree
+buckets encountered: this will have adverse effects on server performance.
+The commands should never be run on a primary and will cause a secondary to
+fall behind on replication. `diskStorage` when run with
+`analyze: "pagesInRAM"` is the exception as it typically returns rapidly and
+may only page in extent headers.
+
+USAGE
+-----
+
+To use the commands and visualizers you need a recent MongoDB Nightly build.
+
+You can download a MongoDB Nightly binary from http://www.mongodb.org/downloads
+(Nightly builds are generated from the unstable branch and should not be
+used in production).
+
+You can enable the experimental commands with
+
+ --enableExperimentalStorageDetailsCmd
+or
+
+ --enableExperimentalIndexStatsCmd
+
+**NOTE: running mongod with these options is unsafe and not advisable for
+production servers.**
+
+If you'd like to run the commands directly within the shell, helpers are
+available.
+
+Json output:
+
+ db.collection.diskStorageStats({...})
+ db.collection.pagesInRAM({...})
+ db.collection.indexStats({index: "index name", ...})
+
+Their counterparts providing human-readable output follow.
+
+ db.collection.getDiskStorageStats({...})
+ db.collection.getPagesInRAM({...})
+ db.collection.getIndexStats({...})
+
+VISUALIZERS
+-----------
+
+To use the visualizers the server needs to be started with the `--rest --jsonp` command
+line flags.
+
+**NOTE: running mongod with these options is unsafe and not advisable for
+production servers.**
+**`--rest` will allow everyone on the same network as the server to query or
+modify data. Please refer to http://en.wikipedia.org/wiki/JSONP#Security_concerns
+for security concerns related to `--jsonp`.**
+
+The *visualizers* provide a nicer graphical representation but are very early stage
+and have only been tested in Chrome.
+The source code for them is available in this repository.
+
+http://10gen-labs.github.com/storage-viz/diskStorage.html displays storage layout
+and usage.
+
+http://10gen-labs.github.com/storage-viz/indexStats.html shows statistics related
+to the indexing btrees.
+
+http://10gen-labs.github.com/storage-viz/pagesInRAM.html reports which parts of a
+collection is currently in ram.
+
+ADDITIONAL
+----------
+
+If you'd like to report a bug or request a new feature,
+please file an issue on our github repository
+(you must be logged into github to do this):
+
+https://github.com/10gen-labs/storage-viz/issues/new
View
111 base.css
@@ -0,0 +1,111 @@
+/*
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* --- GRID --- */
+body {
+ font-family: sans-serif;
+ font-size: 10pt;
+ color: #555;
+ /*padding-left: 20px;*/
+ /*padding-right: 20px;*/
+}
+
+table {
+ font-size: 10pt;
+}
+
+* {
+ margin: 0px;
+ padding: 0px;
+}
+
+#container {
+ margin: 0 auto;
+ min-width: 940px;
+}
+
+.grid-td > .grid-tr,
+#container > .grid-tr {
+ width: 100%;
+ display: table;
+ border-collapse: collapse;
+}
+
+.grid-tr {
+ display: table-row;
+}
+
+.grid-td {
+ display: table-cell;
+ border-right: 1px solid #bbb;
+ border-bottom: 1px solid #bbb;
+ min-height: 10px;
+ padding: 6px 10px 6px 10px;
+ vertical-align: top;
+}
+
+.grid-half {
+ width: 50%;
+}
+
+.grid-td:last-child {
+ border-right: none;
+}
+
+/* --- FORMS --- */
+button {
+ padding: 0.3em;
+}
+
+label {
+ margin-right: 0.5em;
+}
+
+input {
+ margin-right: 1em;
+ border: 1px solid;
+ padding: 1px;
+}
+
+/* --- STYLES --- */
+#header {
+ background-color: rgb(64, 40, 23);
+ color: #ccc;
+ border-bottom: none;
+}
+
+#basicInfoRow {
+ font-weight: bold;
+}
+
+.left-table-header {
+ width: 20px;
+ position: relative;
+}
+
+.left-table-header > span {
+ writing-mode: tb-rl;
+ -webkit-transform: rotate(-90deg);
+ -moz-transform: rotate(-90deg);
+ -o-transform: rotate(-90deg);
+ margin-top: 40px;
+ white-space: nowrap;
+ display: block;
+ width: 20px;
+ height: 20px;
+ top: 30px;
+}
+
View
114 base.js
@@ -0,0 +1,114 @@
+
+/*
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+(function(){
+
+d3.selection.prototype.value = function(val) {
+ if (arguments.length < 1) return this.property('value');
+ else this.property('value', val);
+}
+
+var base = this.base = {};
+
+// Simple JavaScript Templating
+// John Resig - http://ejohn.org/ - MIT Licensed
+var cache = {};
+
+base.tmpl = function Tmpl(str, data){
+ // Figure out if we're getting a template, or if we need to
+ // load the template - and be sure to cache the result.
+ var fn = !/\W/.test(str) ?
+ cache[str] = cache[str] :
+
+ // Generate a reusable function that will serve as a template
+ // generator (and which will be cached).
+ new Function("obj",
+ "var p=[],print=function(){p.push.apply(p,arguments);};" +
+
+ // Introduce the data as local variables using with(){}
+ "with(obj){p.push('" +
+
+ // Convert the template into pure JavaScript
+ str
+ .replace(/[\r\t\n]/g, " ")
+ .split("<%").join("\t")
+ .replace(/((^|%>)[^\t]*)'/g, "$1\r")
+ .replace(/\t=(.*?)%>/g, "',$1,'")
+ .split("\t").join("');")
+ .split("%>").join("p.push('")
+ .split("\r").join("\\'")
+ + "');}return p.join('');");
+
+ // Provide some basic currying to the user
+ return data ? fn( data ) : fn;
+};
+
+base.jsonp = function(url, callbackName) {
+ var script = document.createElement('script');
+ script.src = url + "&limit=1&jsonp=" + callbackName;
+ document.getElementsByTagName('body')[0].appendChild(script);
+};
+
+base.generateFormFields = function(selection, fields, onClick) {
+ fields.map(function(x) {
+ selection.append('label').attr('for', x.name).text(x.desc);
+ var input = selection.append('input').attr('name', x.name).attr('type', x.type);
+ if (x.default_) input.value(x.default_);
+ });
+ selection.append('button').text('submit').on('click', onClick);
+};
+
+base.collectFormValues = function(selection, fields) {
+ var params = {};
+ fields.map(function(x) {
+ params[x.name] = selection.select('input[name=' + x.name + ']').value();
+ });
+ return params;
+};
+
+base.property = function(obj, name, default_) {
+ obj['_' + name] = default_;
+ obj[name] = function(_) {
+ if (!arguments.length) return obj['_' + name];
+ else obj['_' + name] = _;
+ return this;
+ };
+};
+
+base.fmt = {
+ percent: function(val) { return val.toFixed(3) }, //d3.format('.3p'),
+ ratioToPercent: function(val) { return (val * 100).toFixed(1) + '%' },
+ percentAndErr: function(val, err) {
+ return this.percent(val) + ' (&plusmn;' + this.percent(err) + ')';
+ },
+ suffix: function(bytes) {
+ if( bytes < 1024 )
+ return Math.floor( bytes ) + "b";
+ if( bytes < 1024 * 1024 )
+ return Math.floor( bytes / 1024 ) + "kb";
+ if( bytes < 1024 * 1024 * 1024 )
+ return Math.floor( ( Math.floor( bytes / 1024 ) / 1024 ) * 100 ) / 100 + "Mb";
+ return Math.floor( ( Math.floor( bytes / ( 1024 * 1024 ) ) / 1024 ) * 100 ) / 100 + "Gb";
+ },
+ suffixAndBytes: function(val) { return base.fmt.suffix(val) + ' (' + val + ' bytes)' }
+}
+
+base.fmt.stat = {
+ percentAndErr: function(stat) { return base.fmt.percentAndErr(stat.mean, stat.stddev); }
+}
+
+})();
View
230 boxPlot.js
@@ -0,0 +1,230 @@
+/*
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// Based on example code by Mike Bostock at http://mbostock.github.com/d3/ex/box.html
+
+function boxChart() {
+ // assumes datum has the form:
+ // {
+ // count:
+ // kurtosis:
+ // max:
+ // mean:
+ // min:
+ // skewness:
+ // stddev:
+ // quantiles: { .01: _, .02: _, .09: _, .25: _, .50: _, .75: _, .91: _, .98: _ }
+ // }
+
+ base.property(chart, 'big', false);
+ base.property(chart, 'domain', null);
+ base.property(chart, 'scale', null);
+ base.property(chart, 'width', 300);
+ base.property(chart, 'height', 20);
+ base.property(chart, 'fillBar', true);
+ base.property(chart, 'showAxis', true);
+
+ function chart(g) {
+ g.each(function(data) {
+ var g = d3.select(this);
+
+ if (data.count == 0) {
+ return;
+ }
+
+ // Compute the new x-scale.
+ var domain = chart._domain || [data.min, data.max];
+ var width = chart._width;
+ var height = chart._height;
+ var x = null;
+ var ticks = null;
+ if (data.min < data.max) {
+ x = chart._scale || d3.scale.linear()
+ .domain(domain)
+ .range([0, width])
+ .nice()
+ .clamp(true);
+ ticks = x.ticks(6);
+ } else {
+ x = d3.scale.linear()
+ .domain([Math.floor(data.min - data.min / 100),
+ Math.ceil(data.max + data.max / 100)])
+ .range([0, width])
+ .nice()
+ .clamp(true);
+ ticks = x.ticks(3);
+ }
+
+ // Update ticks
+ // var boxTick = g.selectAll("text.box").data(x.ticks(8));
+
+ // boxTick.enter().append("svg:text")
+ // .classed("box", true)
+ // .attr("x", x)
+ // .attr("dx", "0em")
+ // .attr("y", height + 15)
+ // .attr("text-anchor", "middle")
+ // .text(format);
+
+ if (chart._fillBar) {
+ var fillBar = g.selectAll("rect.fill-bar").data([data.mean]);
+
+ fillBar.enter().append("svg:rect")
+ .classed("fill-bar", true)
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", x)
+ .attr("height", height);
+ }
+
+ if (chart._showAxis) {
+ var xAxis = d3.svg.axis()
+ .scale(x)
+ .tickValues(ticks)
+ .tickSubdivide(1)
+ .orient("bottom");
+
+ g.append("g")
+ .attr("transform", "translate(0, " + (height + 3) + ")")
+ .classed("x", true)
+ .classed("axis", true)
+ .call(xAxis);
+ }
+ // else {
+ // g.append("g")
+ // .attr("transform", "translate(0, " + (height + 1) + ")")
+ // .classed("x axis", true)
+ // .append("svg:path").attr("d", "M0,0H" + x(x.domain()[1]));
+ // }
+
+ // var rangeTicks = g.selectAll("line.tick").data(x.ticks(8));
+
+ // rangeTicks.enter().append("svg:line")
+ // .classed("tick", true)
+ // .attr("x1", x)
+ // .attr("y1", height + 2)
+ // .attr("x2", x)
+ // .attr("y2", height + 5);
+
+ if (data.quantiles) {
+ // Update center line: the vertical line spanning the whiskers.
+ var center = g.selectAll("line.center").data([[data.quantiles[.01], data.quantiles[.99]]]);
+
+ center.enter().append("svg:line")
+ .classed("center", true)
+ .attr("y1", height / 2)
+ .attr("x1", function(d) { return x(d[0]); })
+ .attr("y2", height / 2)
+ .attr("x2", function(d) { return x(d[1]); });
+
+ // Update innerquartile box.
+ var box = g.selectAll("rect.box").data([[data.quantiles[.25], data.quantiles[.75]]]);
+
+ box.enter().append("svg:rect")
+ .classed("box", true)
+ .attr("y", 0)
+ .attr("x", function(d) { return x(d[0]); })
+ .attr("height", height)
+ .attr("width", function(d) { return x(d[1]) - x(d[0]); });
+
+ // Update median line.
+ var medianLine = g.selectAll("line.median").data([data.quantiles[.5]]);
+
+ medianLine.enter().append("svg:line")
+ .classed("median", true)
+ .attr("y1", 0)
+ .attr("x1", x)
+ .attr("y2", height)
+ .attr("x2", x);
+
+ var median = g.selectAll("path.median").data([data.quantiles[.5]]);
+
+ median.enter().append("svg:path")
+ .classed("median", true)
+ .attr("d", function(d) { return "M" + (x(d) - 6) + "," + height + "h12l-6,-6z" });
+
+ // Update whiskers.
+ var whisker = g.selectAll("line.whisker").data([data.quantiles[.01], data.quantiles[.99]]);
+
+ whisker.enter().append("svg:line")
+ .classed("whisker", true)
+ .attr("y1", 0)
+ .attr("x1", x)
+ .attr("y2", height)
+ .attr("x2", x);
+ }
+
+ if (data.stddev > 0) {
+ var stddev = g.selectAll("line.stddev")
+ .data([[data.mean - data.stddev, data.mean + data.stddev]]);
+
+ stddev.enter().append("svg:path")
+ .classed("stddev", true)
+ .attr("d", function(d) { return "M" + x(d[0]) + ",6" + "v-6" + "H" + x(d[1]) + "v6" });
+ }
+
+ var mean = g.selectAll("path.mean").data([data.mean]);
+
+ mean.enter().append("svg:path")
+ .classed("mean", true)
+ .attr("d", function(d) { return "M" + (x(d) - 6) + ",0" + "h12l-6,6Z" });
+
+ // Update outliers.
+ // var outlier = g.selectAll("circle.outlier")
+ // .data(outlierIndices, Number);
+
+ // outlier.enter().insert("svg:circle", "text")
+ // .attr("class", "outlier")
+ // .attr("r", 5)
+ // .attr("cx", width / 2)
+ // .attr("cy", function(i) { return x0(d[i]); })
+
+ // Update whisker ticks. These are handled separately from the box
+ // ticks because they may or may not exist, and we want don't want
+ // to join box ticks pre-transition with whisker ticks post-.
+ // var whiskerTick = g.selectAll("text.whisker")
+ // .data(whiskerData || []);
+
+ // whiskerTick.enter().append("svg:text")
+ // .attr("class", "whisker")
+ // .attr("dy", ".3em")
+ // .attr("dx", 6)
+ // .attr("x", width)
+ // .attr("y", x0)
+ // .text(format)
+ // .style("opacity", 1e-6)
+ // .transition()
+ // .duration(duration)
+ // .attr("y", x1)
+ // .style("opacity", 1);
+
+ // whiskerTick.transition()
+ // .duration(duration)
+ // .text(format)
+ // .attr("y", x1)
+ // .style("opacity", 1);
+
+ // whiskerTick.exit().transition()
+ // .duration(duration)
+ // .attr("y", x1)
+ // .style("opacity", 1e-6)
+ // .remove();
+ });
+ }
+
+ return chart;
+};
+
View
151,324 btree.bson
151,324 additions, 0 deletions not shown
View
188 btree.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
+ <script type="text/javascript" src="d3.v2.js"></script>
+ <style type="text/css">
+
+
+.node circle {
+ cursor: pointer;
+ fill: #fff;
+ stroke: steelblue;
+ stroke-width: 1.5px;
+}
+
+.node text {
+ font-size: 11px;
+}
+
+path.link {
+ fill: none;
+ stroke: #ccc;
+ stroke-width: 1.5px;
+}
+
+ </style>
+ </head>
+ <body>
+ <div id="body">
+ <div id="footer">
+ <div class="hint">click expand or collapse</div>
+ </div>
+ </div>
+ <script type="text/javascript">
+
+var m = [20, 120, 20, 300],
+ w = 4000 - m[1] - m[3],
+ h = 8000 - m[0] - m[2],
+ i = 0,
+ root;
+
+var tree = d3.layout.tree()
+ .size([h, w]);
+
+var diagonal = d3.svg.diagonal()
+ .projection(function(d) { return [d.y, d.x]; });
+
+var vis = d3.select("#body").append("svg:svg")
+ .attr("width", w + m[1] + m[3])
+ .attr("height", h + m[0] + m[2])
+ .append("svg:g")
+ .attr("transform", "translate(" + m[3] + "," + m[0] + ")");
+
+d3.json("btree.json", function(json) {
+ root = json.head;
+ console.log(root);
+ root.x0 = h / 2;
+ root.y0 = 0;
+
+ function toggleAll(d) {
+ if (d.children) {
+ d.children.forEach(toggleAll);
+ toggle(d);
+ }
+ }
+
+ // Initialize the display to show a few nodes.
+ root.children.forEach(toggleAll);
+ console.log(root);
+
+ update(root);
+});
+
+function update(source) {
+ var duration = d3.event && d3.event.altKey ? 5000 : 500;
+
+ // Compute the new tree layout.
+ var nodes = tree.nodes(root).reverse();
+
+ // Normalize for fixed-depth.
+ nodes.forEach(function(d) { d.y = d.depth * 300; });
+
+ // Update the nodes…
+ var node = vis.selectAll("g.node")
+ .data(nodes, function(d) { return d.id || (d.id = ++i); });
+
+ // Enter any new nodes at the parent's previous position.
+ var nodeEnter = node.enter().append("svg:g")
+ .attr("class", "node")
+ .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
+ .on("click", function(d) { toggle(d); update(d); });
+
+ nodeEnter.append("svg:circle")
+ .attr("r", 1e-6)
+ .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
+
+ nodeEnter.append("svg:text")
+ .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
+ .attr("dy", ".0em")
+ .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
+ .text(function(d) { return d.firstKey[""]; });
+ nodeEnter.append("svg:text")
+ .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
+ .attr("dy", "1em")
+ .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
+ .text(function(d) { return d.lastKey[""]; });
+
+ // Transition nodes to their new position.
+ var nodeUpdate = node.transition()
+ .duration(duration)
+ .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
+
+ nodeUpdate.select("circle")
+ .attr("r", 4.5)
+ .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
+
+ nodeUpdate.select("text")
+ .style("fill-opacity", 1);
+
+ // Transition exiting nodes to the parent's new position.
+ var nodeExit = node.exit().transition()
+ .duration(duration)
+ .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
+ .remove();
+
+ nodeExit.select("circle")
+ .attr("r", 1e-6);
+
+ nodeExit.select("text")
+ .style("fill-opacity", 1e-6);
+
+ // Update the links…
+ var link = vis.selectAll("path.link")
+ .data(tree.links(nodes), function(d) { return d.target.id; });
+
+ // Enter any new links at the parent's previous position.
+ link.enter().insert("svg:path", "g")
+ .attr("class", "link")
+ .attr("d", function(d) {
+ var o = {x: source.x0, y: source.y0};
+ return diagonal({source: o, target: o});
+ })
+ .transition()
+ .duration(duration)
+ .attr("d", diagonal);
+
+ // Transition links to their new position.
+ link.transition()
+ .duration(duration)
+ .attr("d", diagonal);
+
+ // Transition exiting nodes to the parent's new position.
+ link.exit().transition()
+ .duration(duration)
+ .attr("d", function(d) {
+ var o = {x: source.x, y: source.y};
+ return diagonal({source: o, target: o});
+ })
+ .remove();
+
+ // Stash the old positions for transition.
+ nodes.forEach(function(d) {
+ d.x0 = d.x;
+ d.y0 = d.y;
+ });
+}
+
+// Toggle children.
+function toggle(d) {
+ if (d.children) {
+ d._children = d.children;
+ d.children = null;
+ } else {
+ d.children = d._children;
+ d._children = null;
+ }
+}
+
+function collapse(d) {
+ if (d.children) {
+ d._children = d.children;
+ d.children = null;
+ }
+}
+
+ </script>
+ </body>
+</html>
View
7,062 d3.v2.js
7,062 additions, 0 deletions not shown
View
94 diskStorage.css
@@ -0,0 +1,94 @@
+/* index.css */
+
+/**
+* Copyright (C) 2012 10gen Inc.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License, version 3,
+* as published by the Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#extentSummaryRow {
+ background: white;
+ z-index: 10;
+}
+
+#extentSummary {
+ padding: 10px;
+}
+
+.extentGraph {
+ overflow: auto;
+}
+
+.extentRow.highlighted > div:first-child {
+ background-color: #ffc;
+}
+
+rect.spaceUsageBar {
+ fill: white;
+}
+
+rect.bsonBytes {
+ fill: #88f;
+}
+
+rect.recBytes {
+ fill: #bbf;
+}
+
+#infoBox table td {
+ padding-right: 15px;
+ text-align: right;
+}
+
+#legend .col {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+}
+
+.axis path {
+ fill: none;
+ stroke: black;
+ shape-rendering: crispEdges;
+}
+
+.axis line {
+ stroke: black;
+ shape-rendering: crispEdges;
+}
+
+path.frame {
+ stroke: black;
+ fill: none;
+ shape-rendering: crispEdges;
+}
+
+.extentSummary {
+ width: 50px;
+ min-width: 50px;
+ max-width: 50px;
+ text-align: center;
+}
+
+rect.border {
+ fill: none;
+}
+
+.summaryBar rect.border {
+ stroke: black;
+ shape-rendering: crispEdges;
+}
+
+.onDiskBytes {
+ margin-bottom: 5px;
+}
View
63 diskStorage.html
@@ -0,0 +1,63 @@
+<!--
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+ <head>
+ <title>mongovis visualizer</title>
+ <script type="text/javascript" src="d3.v2.js"></script>
+ <script type="text/javascript" src="base.js"></script>
+ <link rel="stylesheet" href="base.css" />
+ <link rel="stylesheet" href="diskStorage.css" />
+ </head>
+ <body>
+ <div id="container">
+ <div class="grid-tr">
+ <div id="header" class="grid-td">
+ mongoDB :: collection.storageDetails({analyze: 'diskStorage'}) command (mongod should be started with --rest --jsonp)
+ </div>
+ <div id="legend" class="grid-td">
+ <span class="col" style="background-color: #88f;"></span> bson
+ <span class="col" style="background-color: #bbf;"></span> padding+header
+
+ </div>
+ </div>
+ <div class="grid-tr">
+ <div id="requestForm" class="grid-td">
+ </div>
+ </div>
+ <div class="grid-tr">
+ <div id="resultString" class="grid-td">
+ </div>
+ </div>
+ <div id="basicInfoRow" class="grid-tr">
+ </div>
+ <div id="spaceFiller"></div>
+ <div id="extentSummaryRow" class="grid-tr">
+ <div id="extentSummaryHeader" class="grid-td left-table-header"
+ style="display:none;">
+
+ <span>extents</span>
+ </div>
+ <div id="extentSummary" class="grid-td" style="display:none;">
+ </div>
+ <div id="infoBox" class="grid-td">
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript" src="diskStorage.js"></script>
+ </body>
+</html>
+
View
329 diskStorage.js
@@ -0,0 +1,329 @@
+// index.js
+
+/**
+* Copyright (C) 2012 10gen Inc.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License, version 3,
+* as published by the Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+(function() {
+
+var URL_TEMPLATE = "http://<%=host%>/<%=database%>/$cmd/?filter_storageDetails=<%=collection%>" +
+ "&filter_analyze=diskStorage" +
+ // "<%=(extent) ? '&filter_extent=' + extent : ''%>" +
+ "<%=(granularity) ? '&filter_granularity=' + (granularity * 1024) : ''%>" +
+ "<%=(numberOfSlices) ? '&filter_numberOfSlices=' + numberOfSlices : ''%>";
+
+var REQUEST_FORM_FIELDS = [
+ { name: 'host', desc: 'host', type: 'text', default_: 'localhost:28017' },
+ { name: 'database', desc: 'db', type: 'text', default_: 'test' },
+ { name: 'collection', desc: 'collection', type: 'text', default_: 'test' },
+ // { name: 'extent', desc: 'extent (opt)', type: 'text', default_: '' },
+ { name: 'granularity', desc: 'granularity (Kb) (opt)', type: 'text', default_: '2048' },
+ { name: 'numberOfSlices', desc: 'number of slices (opt)', type: 'text', default_: '' }
+]
+
+function layoutHacks() {
+ console.log('hacks');
+ d3.selectAll('.extentGraph')
+ .style('max-width', document.documentElement.clientWidth - 70);
+
+}
+
+function setUp() {
+ var requestForm = d3.select('#requestForm');
+ base.generateFormFields(requestForm, REQUEST_FORM_FIELDS, function() {
+ var reqParams = base.collectFormValues(requestForm, REQUEST_FORM_FIELDS);
+ console.log(reqParams);
+ var url = base.tmpl(URL_TEMPLATE, reqParams);
+ d3.select('#resultString').text('fetching ' + url + '...');
+ base.jsonp(url, 'handleData');
+ });
+
+ var $extentSummaryRow = d3.select('#extentSummaryRow');
+ var $spaceFiller = d3.select('#spaceFiller');
+
+ d3.select(window).on('scroll', function(a) {
+ if (window.scrollY > 85) {
+ $extentSummaryRow.style('position', 'fixed');
+ $extentSummaryRow.style('top', '0');
+ $spaceFiller.style('min-height', $extentSummaryRow.node().clientHeight);
+ } else {
+ $extentSummaryRow.style('position', null);
+ $extentSummaryRow.style('top', null);
+ $spaceFiller.style('min-height', null);
+ }
+ });
+
+ d3.select(window).on('resize', layoutHacks);
+}
+
+this.handleData = function handleData(data) {
+ _data = data.rows[0];
+ if (!_data.ok) {
+ d3.select('#resultString').text('error: ' + _data.errmsg);
+ return;
+ }
+ d3.select('#resultString').text('executed command with params ' + JSON.stringify(data.query) +
+ ', rendering');
+ console.log(_data);
+
+ d3.select('#resultString').text('executed command ' + JSON.stringify(data.query));
+
+ var basicInfoRow = d3.select('#basicInfoRow');
+ basicInfoRow.selectAll('*').remove();
+
+ var SUMMARY_BAR_WIDTH = 20;
+ var BAR_HEIGHT = 120;
+ var BAR_WIDTH = 6;
+
+ var $extentSummary = d3.select('#extentSummary');
+ $extentSummary.selectAll('*').remove();
+ d3.select('#container').selectAll('.extentRow').remove();
+ var $infoBox = d3.select('#infoBox');
+ $infoBox.selectAll('*').remove();
+ $infoBox.append('div').text('click on bars for details');
+
+ var updateInfoBox = function(datum) {
+ $infoBox.selectAll('*').remove();
+ $infoBox.datum(datum).call(infoBox());
+ };
+
+ if (_data.extents) {
+ $extentSummary.style('display', null);
+ d3.select('#extentSummaryHeader').style('display', null);
+ var summaryX = function(d, i) { return i * (SUMMARY_BAR_WIDTH + 10) };
+ $extentSummary
+ .append('svg')
+ .attr('width', summaryX(null, _data.extents.length))
+ .attr('height', BAR_HEIGHT + 1)
+ .selectAll('g')
+ .data(_data.extents)
+ .enter()
+ .append('svg:g')
+ .classed('summaryBar', true)
+ .attr('transform', function(d, i) { return 'translate(' + summaryX(d, i) + ', 0)' })
+ .call(spaceUsageBar().width(SUMMARY_BAR_WIDTH).height(BAR_HEIGHT))
+ .on('click', function(datum, i) {
+ d3.selectAll('.extentRow')
+ .classed('highlighted', false);
+ d3.select('.extentRow-' + i)
+ .classed('highlighted', true)
+ .node().scrollIntoView(false);
+ updateInfoBox(datum);
+ });
+
+ var extentRowEnter = d3.select('#container')
+ .selectAll('.extentRow')
+ .data(_data.extents)
+ .enter()
+ .append('div')
+ .attr('class', function(d, i) { return 'extentRow-' + i })
+ .classed('grid-tr extentRow', true);
+
+ extentRowEnter.append('div')
+ .classed('grid-td left-table-header', true)
+ .append('span')
+ .text(function(d, i) { return 'extent ' + (i + 1) });
+
+ var extentSummary = extentRowEnter.append('div')
+ .classed('grid-td extentSummary', true);
+
+ extentSummary.selectAll('div.onDiskBytes')
+ .data(function(d) { return [d] })
+ .enter()
+ .append('div')
+ .classed('onDiskBytes', true)
+ .text(function(d) { return base.fmt.suffix(d.onDiskBytes) });
+
+ extentSummary.append('svg')
+ .attr('width', SUMMARY_BAR_WIDTH + 2)
+ .attr('height', BAR_HEIGHT + 1)
+ .selectAll('g.summaryBar')
+ .data(function(d) { return [d] })
+ .enter()
+ .append('svg:g')
+ .classed('summaryBar', true)
+ .call(spaceUsageBar().width(SUMMARY_BAR_WIDTH).height(BAR_HEIGHT))
+ .on('click', updateInfoBox);
+
+ extentRowEnter.append('div')
+ .classed('grid-td extentGraph', true)
+ .append('svg')
+ .attr('width', function(d, i) {
+ return (d.slices ? d.slices.length : 0) * BAR_WIDTH + 40;
+ })
+ .attr('height', BAR_HEIGHT + 20)
+ .append('g')
+ .attr('transform', 'translate(10, 0)')
+ .call(slicesUsagePlot().sliceWidth(BAR_WIDTH)
+ .height(BAR_HEIGHT)
+ .onClick(updateInfoBox));
+ } else {
+ d3.select('#resultString').text('single extent mode is not supported yet');
+ }
+
+ layoutHacks();
+};
+
+function slicesUsagePlot() {
+
+ base.property(chart, 'sliceWidth', 10);
+ base.property(chart, 'height', 80);
+ base.property(chart, 'onClick', function() {});
+
+ function chart(g) {
+ g.each(function(data) {
+ var g = d3.select(this);
+
+ if (!data.slices) {
+ return;
+ }
+
+ var length = data.slices.length;
+
+ var x = d3.scale.linear().domain([0, length]).range([0, length * chart._sliceWidth]);
+
+ var xAxis = d3.svg.axis()
+ .scale(x)
+ .tickValues(d3.range(0, data.slices.length, 16))
+ .tickFormat(function(d) { return base.fmt.suffix(d * data.slices[0].onDiskBytes) })
+ .orient('bottom');
+
+ g.append('g')
+ .attr('transform', 'translate(0, ' + chart._height + ')')
+ .classed('x axis', true)
+ .call(xAxis);
+
+ g.selectAll('g.sliceBar')
+ .data(function(d) { return (d.slices ? d.slices : []) })
+ .enter()
+ .append('svg:g')
+ .classed('sliceBar', true)
+ .attr('transform', function(d, i) { return 'translate(' + x(i) + ', 0)' })
+ .call(spaceUsageBar().width(chart._sliceWidth - 1).height(chart._height))
+ .on('click', chart._onClick);
+
+ g.append('svg:path')
+ .classed('frame', true)
+ .attr('d', 'M0,' + chart._height + 'V0' + 'h' + x(length) + 'v' + chart._height);
+
+ });
+ }
+
+ return chart;
+}
+
+function spaceUsageBar() {
+ // assumes datum has the form:
+ // {
+ // numEntries:
+ // outOfOrderRecs:
+ // onDiskBytes:
+ // recBytes:
+ // bsonBytes:
+ // isCapped:
+ // (opt) freeRecsPerBucket: [arr]
+ // }
+
+ base.property(chart, 'width', 6);
+ base.property(chart, 'height', 100);
+
+ function chart(g) {
+ g.each(function(data) {
+ var g = d3.select(this);
+
+ var y = d3.scale.linear().domain([0, data.onDiskBytes]).range([chart._height, 0]);
+ var yHeight = d3.scale.linear().domain([0, data.onDiskBytes]).range([0, chart._height]);
+
+ g.append('rect')
+ .classed('spaceUsageBar', true)
+ .attr('height', chart._height)
+ .attr('width', chart._width);
+
+ g.selectAll('rect.recBytes')
+ .data([data.recBytes])
+ .enter().append('svg:rect')
+ .classed('recBytes', true)
+ .attr('y', y)
+ .attr('height', yHeight)
+ .attr('width', chart._width);
+
+ g.selectAll('rect.bsonBytes')
+ .data([data.bsonBytes])
+ .enter().append('svg:rect')
+ .classed('bsonBytes', true)
+ .attr('y', y)
+ .attr('height', yHeight)
+ .attr('width', chart._width);
+
+ g.append('rect')
+ .classed('border', true)
+ .attr('height', chart._height)
+ .attr('width', chart._width);
+
+ });
+ }
+
+ return chart;
+}
+
+function infoBox() {
+
+ function chart(g) {
+ g.each(function(data) {
+ var g = d3.select(this);
+ g.style('vertical-align', 'top');
+
+ var summaryTable = g.append('table').style('display', 'inline-block');
+ summaryTable.style('vertical-align', 'top');
+ [
+ ["size", "<%=base.fmt.ratioToPercent(1)%>",
+ "<%=base.fmt.suffixAndBytes(onDiskBytes)%>"],
+ ["records", "", "<%=numEntries%>"],
+ ["record size", "<%=base.fmt.ratioToPercent(recBytes / onDiskBytes)%>",
+ "<%=base.fmt.suffixAndBytes(recBytes)%>"],
+ ["bson size", "<%=base.fmt.ratioToPercent(bsonBytes / onDiskBytes)%>",
+ "<%=base.fmt.suffixAndBytes(bsonBytes)%>"]
+ ].map(function(x) {
+ var row = summaryTable.append('tr');
+ x.map(function(d) { row.append('td').text(base.tmpl("<%=''%>" + d, data)) });
+ });
+
+ var delRecDiv = g.append('div').style('display', 'inline-block');
+ delRecDiv.style('border', '1px solid #aaa');
+ delRecDiv.append('span').style('text-align', 'center')
+ .style('font-weight', 'bold').text('deleted records');
+ delRecDiv.append('br');
+ var i = 0;
+ data.freeRecsPerBucket.map(function(x) {
+ var ddd = delRecDiv.append('div')
+ .style('display', 'inline-block')
+ .style('padding', '5px');
+ ddd.append('span').text(base.fmt.suffix(Math.pow(2, i + 5)) + ": ");
+ ddd.append('span').text(x);
+ i++;
+ if (i % 5 == 0) {
+ delRecDiv.append('br');
+ }
+ });
+ row.append('td').text('a');
+ });
+ }
+
+ return chart;
+}
+
+setUp();
+
+})();
View
53 indexStats.css
@@ -0,0 +1,53 @@
+
+/*
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+.level-summary {
+ width: 325px;
+ min-width: 325px;
+}
+
+.expanded-container {
+ overflow: auto;
+ white-space: nowrap;
+ min-width: 500px;
+}
+
+.expanded-node {
+ vertical-align: top;
+ display: inline-block;
+ border: 1px dashed #bbb;
+ padding: 6px;
+ margin-right: 12px;
+}
+
+.expanded-header {
+ border-bottom: 1px dashed #bbb;
+ padding-bottom: 5px;
+ margin-bottom: 5px;
+}
+
+/* --- BOX-PLOT --- */
+.boxplot { font: 10px sans-serif; }
+.boxplot line.whisker, line.center, line.median, .boxplot rect.box, .boxplot circle { stroke: #000; fill: #fff; }
+.boxplot line { stroke-width: 1px; shape-rendering: crispEdges; }
+.boxplot rect.fill-bar { stroke: none; fill: #ccf; }
+.boxplot .center { stroke-dasharray: 3 3; }
+.boxplot .outlier { stroke: #ccc; fill: none; }
+.boxplot path.mean { stroke: none; fill: #f00; }
+.boxplot path.median { stroke:none; fill: #00f; }
+.boxplot path.stddev { fill:none; stroke: #f00; stroke-width: 1px; shape-rendering: crispEdges; }
+.boxplot .axis path, .boxplot .axis line { fill: none; stroke: #000; stroke-width: 1px; shape-rendering: crispEdges; }
View
54 indexStats.html
@@ -0,0 +1,54 @@
+<!--
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+ <head>
+ <title>mongovis visualizer</title>
+ <script type="text/javascript" src="d3.v2.js"></script>
+ <script type="text/javascript" src="base.js"></script>
+ <script type="text/javascript" src="boxPlot.js"></script>
+ <link rel="stylesheet" href="base.css" />
+ <link rel="stylesheet" href="indexStats.css" />
+ </head>
+ <body>
+ <div id="container">
+ <div class="grid-tr">
+ <div id="header" class="grid-td">
+ mongoDB :: collection.indexStats command (mongod should be started with --rest --jsonp)
+ </div>
+ </div>
+ <div class="grid-tr">
+ <div id="requestForm" class="grid-td">
+ </div>
+ </div>
+ <div class="grid-tr">
+ <div id="resultString" class="grid-td">
+ </div>
+ </div>
+ <div id="basicInfoRow" class="grid-tr">
+ </div>
+ <div class="grid-tr">
+ <div id="curNodeStats" class="grid-td grid-half"></div>
+ <div id="detailedStats" class="grid-td grid-half"></div>
+ </div>
+ <div class="grid-tr">
+ <div id="levels" class="grid-td">
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript" src="indexStats.js"></script>
+ </body>
+</html>
View
271 indexStats.js
@@ -0,0 +1,271 @@
+
+/*
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+(function() {
+
+// TODO: node expansion from the visualizer can be re-enabled once the rest api can safely accept
+// a filter array.
+var URL_TEMPLATE = "http://<%=host%>/<%=database%>/$cmd/?filter_indexStats=<%=collection%>" +
+ "&filter_index=<%=index%>"; //&filter_arr_expandNodes=<%=expandNodes%>";
+
+var REQUEST_FORM_FIELDS = [
+ { name: 'host', desc: 'host', type: 'text', default_: 'localhost:28017' },
+ { name: 'database', desc: 'db', type: 'text', default_: 'test' },
+ { name: 'collection', desc: 'collection', type: 'text', default_: 'test' },
+ { name: 'index', desc: 'index', type: 'text', default_: '_id_' },
+ // { name: 'expandNodes', desc: 'expand nodes (e.g. 0,2,3)', type: 'text', default_: '0' }
+]
+
+function layoutHacks() {
+ d3.selectAll('.expanded-container').style('max-width',
+ document.documentElement.clientWidth - 450);
+}
+
+function setUp() {
+ var requestForm = d3.select('#requestForm');
+ base.generateFormFields(requestForm, REQUEST_FORM_FIELDS, function() {
+ var url = base.tmpl(URL_TEMPLATE, base.collectFormValues(requestForm, REQUEST_FORM_FIELDS));
+ d3.select('#resultString').text('fetching ' + url + '...');
+ base.jsonp(url, 'handleData');
+ });
+ d3.select(window).on('resize', layoutHacks);
+}
+
+var _data;
+
+// expose so it can be invoked by the jsonp response script
+this.handleData = function handleData(data) {
+ _data = data.rows[0];
+ if (!_data.ok) {
+ d3.select('#resultString').text('error: ' + _data.errmsg);
+ return;
+ }
+ d3.select('#resultString').text('executed command with params ' + JSON.stringify(data.query) +
+ ', rendering');
+ console.log(_data);
+
+ _data.keyPattern = JSON.stringify(_data.keyPattern);
+ d3.select('#resultString').text('executed command ' + JSON.stringify(data.query));
+
+ var basicInfoRow = d3.select('#basicInfoRow');
+ basicInfoRow.selectAll('*').remove();
+ [
+ 'index "<%=name%>"',
+ 'key pattern: <%=keyPattern%>',
+ 'storage namespace: <%=storageNs%>',
+ '<%=depth%> deep',
+ 'each bucket body is <%=bucketBodyBytes%> bytes',
+ (_data.isIdIndex ? ' this is an _id index' : ' ')
+ ].map(function(x) {
+ basicInfoRow.append('div').classed('grid-td', true).text(base.tmpl(x, _data))
+ });
+
+ var dataArrays = [[_data.overall], _data.perLevel, _data.expandedNodes];
+ dataArrays.map(function(arr) {
+ // generate calculated data fields
+ });
+
+ var $curNodeStats = d3.select('#curNodeStats');
+ $curNodeStats.selectAll('*').remove();
+ $curNodeStats.datum(_data.overall).call(statsDisplay().big(true));
+
+ var levelAndExpandedData = [];
+ for (var d = 0; d < _data.perLevel.length; ++d) {
+ levelAndExpandedData.push({
+ level: _data.perLevel[d],
+ expandedNodes: ((_data.expandedNodes && _data.expandedNodes[d]) ?
+ _data.expandedNodes[d] : [])
+ });
+ }
+
+ // debugger;
+ var $levels = d3.select('#levels');
+ $levels.selectAll('*').remove();
+ var levelEnter = $levels.selectAll('.level')
+ .data(levelAndExpandedData)
+ .enter()
+ .append('div')
+ .classed('level grid-tr', true);
+
+ levelEnter.append('div')
+ .classed('grid-td', true)
+ .classed('left-table-header', true)
+ .append('span')
+ .text(function(d, i) { return 'depth ' + i });
+
+ levelEnter.append('div')
+ .datum(function(d) { return d.level })
+ .classed('grid-td level-summary', true)
+ .call(statsDisplay().big(false).width(300));
+
+ levelEnter.append('div')
+ .classed('grid-td expanded-container', true)
+ .selectAll('.expanded-node')
+ .data(function(d) { return d.expandedNodes })
+ .enter()
+ .append('div')
+ .classed('expanded-node', true)
+ .call(expandedDisplay());
+
+ layoutHacks();
+}
+
+function expandedDisplay() {
+ function chart(selection) {
+ selection.each(function(data) {
+ var $this = d3.select(this);
+ $this.selectAll('*').remove();
+
+ if (!data.nodeInfo) {
+ $this.append('div').text('empty child');
+ return;
+ }
+
+ var htmlTemplate = 'child #<b><%=childNum%></b>, <b><%=keyCount%></b> keys ' +
+ '(<b><%=usedKeyCount%></b> used)';
+ if (data.nodeInfo.firstKey) {
+ htmlTemplate += '<br/><%=JSON.stringify(firstKey)%> '
+ }
+ if (data.nodeInfo.lastKey) {
+ htmlTemplate += '<br/>--&gt; <%=JSON.stringify(lastKey)%>';
+ }
+
+ $this.append('div')
+ .classed('expanded-header', true)
+ .html(base.tmpl(htmlTemplate, data.nodeInfo));
+
+ $this.append('div').text('subtree stats');
+ $this.append('div')
+ .call(statsDisplay().big(false).width(280));
+ });
+ }
+
+ return chart;
+}
+
+function statsDisplay() {
+
+ function chart(selection) {
+ selection.each(function(data) {
+ var $this = d3.select(this);
+ if (data.numBuckets == 0) {
+ $this.append('div').html('<b>0</b> buckets');
+ return;
+ }
+ $this.append('div').html(base.tmpl(
+ '<b><%=numBuckets%></b> buckets' +
+ ', on average <b><%=base.fmt.stat.percentAndErr(fillRatio)%></b> full' +
+ '<br/><b><%=base.fmt.stat.percentAndErr(keyNodeRatio)%></b> key nodes' +
+ ', <b><%=base.fmt.stat.percentAndErr(bsonRatio)%></b> bson keys'
+ , data));
+ var ratios = [
+ { desc: '% full', data: data.fillRatio },
+ { desc: '% bson keys', data: data.bsonRatio },
+ { desc: '% key nodes', data: data.keyNodeRatio }
+ ];
+
+ var graphWidth = chart._width - 110;
+
+ var ratiosSvg = $this.append('div').append('svg')
+ .classed('boxplot', true)
+ .attr('width', chart._width)
+ .attr('height', 40 * ratios.length);
+
+ var ratiosG = ratiosSvg
+ .selectAll('g')
+ .data(ratios);
+
+ var ratioBoxChart = boxChart()
+ .domain([0, 1])
+ .showAxis(false)
+ .width(graphWidth)
+ .big(true);
+
+ var ratiosEnter = ratiosG.enter()
+
+ var y = function(d, i) { return (i * 25 + 10) };
+
+ ratiosEnter.append('svg:text')
+ .attr('x', 90)
+ .attr('y', y)
+ .attr('dy', 13)
+ .attr('text-anchor', 'end')
+ .text(function(d) { return d.desc });
+
+ ratiosEnter.append('svg:g')
+ .attr('transform', function(d, i) { return 'translate(100, ' + y(d, i) + ')' })
+ .datum(function(d) { return d.data })
+ .call(ratioBoxChart);
+
+ var x = d3.scale.linear().domain([0, 1]).range([0, graphWidth]);
+ var xAxis = d3.svg.axis()
+ .scale(x)
+ .tickValues(x.ticks(8))
+ .orient('bottom');
+
+ ratiosSvg.append('svg:g')
+ .classed('x axis', true)
+ .attr('transform', 'translate(100, ' + (ratios.length * 25 + 10) + ')')
+ .call(xAxis);
+
+ var counts = [
+ { desc: 'used keys', data: data.usedKeyCount },
+ { desc: 'keys', data: data.keyCount }
+ ]
+
+ var countBoxChart = ratioBoxChart
+ .fillBar(false)
+ .domain(null)
+ .showAxis(true);
+
+ var countsSvg = $this.append('div').append('svg')
+ .classed('boxplot', true)
+ .attr('width', chart._width)
+ .attr('height', 40 * ratios.length);
+
+ var countsG = countsSvg
+ .selectAll('g')
+ .data(counts);
+
+ var countsEnter = countsG.enter();
+
+ y = function(d, i) { return (i * 50 + 10) };
+
+ countsEnter.append('svg:text')
+ .attr('x', 90)
+ .attr('y', y)
+ .attr('dy', 13)
+ .attr('text-anchor', 'end')
+ .text(function(d) { return d.desc });
+
+ countsEnter.append('svg:g')
+ .attr('transform', function(d, i) { return 'translate(100, ' + y(d, i) + ')' })
+ .datum(function(d) { return d.data })
+ .call(countBoxChart);
+
+ });
+ }
+
+ base.property(chart, 'big', false);
+ base.property(chart, 'width', 450);
+
+ return chart;
+}
+
+setUp();
+
+})();
View
9,301 jquery.js
9,301 additions, 0 deletions not shown
View
34 memcurve.html
@@ -0,0 +1,34 @@
+<!--
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+ <head>
+ <title>mongovis visualizer</title>
+ <script type="text/javascript" src="jquery.js"></script>
+ <script type="text/javascript" src="d3.v2.js"></script>
+ <script type="text/javascript" src="underscore.js"></script>
+ <script type="text/javascript" src="memcurve.js"></script>
+ </head>
+ <body>
+ <input type="file" id="fileSelect" onchange="handleFiles(this.files)"></input>
+ <button id="clear" onclick="document.location.reload(true)">Clear</button>
+ <div id="wrapper">
+ <div id="memcurve">
+ <canvas id="memcurvecanvas" width="1000" height="1000"></canvas>
+ </div>
+ </div>
+ </body>
+</html>
View
101 memcurve.js
@@ -0,0 +1,101 @@
+// memcurve.js
+
+/**
+* Copyright (C) 2012 10gen Inc.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License, version 3,
+* as published by the Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+var _data;
+
+/*function handleFiles(files) {*/
+/*var reader = new FileReader();*/
+/*var text = reader.readAsText(files[0]);*/
+/*reader.onload = (function(file) {*/
+/*_data = JSON.parse(file.target.result);*/
+/*console.log(_data);*/
+/*render(_data);*/
+/*});*/
+/*d3.select("#fileSelect").style({display: "none"});*/
+/*}*/
+
+function hilbert_d2xy(n, d) {
+ var t = d;
+ var x = 0, y = 0;
+ var rx, ry;
+ for (var s = 1; s < n; s = s * 2) {
+ rx = 1 & (t >> 1);
+ ry = 1 & (t ^ rx);
+ var xy = rot(s, x, y, rx, ry);
+ x = xy.x;
+ y = xy.y;
+ x += s * rx;
+ y += s * ry;
+ t = (t >> 2);
+ }
+ return {
+ x: x,
+ y: y
+ };
+}
+
+function rot(n, x, y, rx, ry) {
+ if (ry == 0) {
+ if (rx == 1) {
+ x = n - 1 - x;
+ y = n - 1 - y;
+ }
+
+ //Swap x and y
+ return {
+ x: y,
+ y: x
+ };
+ }
+ return {
+ x: x,
+ y: y
+ };
+}
+
+function render(data) {
+ var canvas = document.getElementById("memcurvecanvas");
+ var ctx = canvas.getContext("2d");
+ var N = Math.pow(2, 9);
+ var CNT = Math.pow(N, 2);
+ console.log(CNT);
+ var imgData = ctx.getImageData(0, 0, N, N);
+ console.log(imgData.width + " " + imgData.height);
+ var aaa = Math.floor(Math.random() * 100 * (Math.random() * 10));
+ console.log(aaa);
+ var up = true;
+ for (var i = 0; i < CNT; ++i) {
+ --aaa;
+ if (aaa <= 0) {
+ up = !up;
+ aaa = Math.floor(Math.random() * 100 * (Math.random() * 10));
+ }
+ var pt = hilbert_d2xy(N, i);
+ //var col = d3.hsl(Math.floor((1 - i / CNT) * 360), 1, 0.5).rgb();
+ var pxid = (pt.x + pt.y * imgData.width) * 4;
+ var val = 55 + Math.floor(i / CNT * 200);
+ var col = {r: up ? 0 : 255, g: up ? 0 : 255, b: up ? val : 255, a: 255};
+ imgData.data[pxid] = col.r;
+ imgData.data[pxid + 1] = col.g;
+ imgData.data[pxid + 2] = col.b;
+ imgData.data[pxid + 3] = col.a;
+ }
+ ctx.putImageData(imgData, 0, 0);
+}
+
+$(function() { render(null) });
View
75 pagesInRAM.css
@@ -0,0 +1,75 @@
+/**
+* Copyright (C) 2012 10gen Inc.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License, version 3,
+* as published by the Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+div.extentChart {
+ width: 50px;
+ min-width: 50px;
+ max-width: 50px;
+ text-align: center;
+}
+
+div.slicesGraph {
+ overflow: auto;
+}
+
+rect.border {
+ fill: white;
+ stroke: #777;
+ shape-rendering: crispEdges;
+}
+
+rect.inRAM {
+ fill: #777;
+ stroke: none;
+}
+
+rect.slice {
+ fill: #00f;
+ stroke: none;
+}
+
+path.frame {
+ stroke: black;
+ fill: none;
+ shape-rendering: crispEdges;
+}
+
+.axis path {
+ fill: none;
+ stroke: black;
+ shape-rendering: crispEdges;
+}
+
+.axis line.tick {
+ stroke: black;
+ shape-rendering: crispEdges;
+}
+
+.extentRow.highlighted > div:first-child {
+ background-color: #ffc;
+}
+
+.extentSize {
+ margin-bottom: 5px;
+}
+
+.extentPercent {
+ margin-top: 5px;
+}
+
+#extentsSummary svg {
+ margin: 10px;
+}
View
60 pagesInRAM.html
@@ -0,0 +1,60 @@
+<!--
+Copyright (C) 2012 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License, version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+ <head>
+ <title>mongovis visualizer</title>
+ <script type="text/javascript" src="d3.v2.js"></script>
+ <script type="text/javascript" src="base.js"></script>
+ <link rel="stylesheet" href="base.css" />
+ <link rel="stylesheet" href="pagesInRAM.css" />
+ </head>
+ <body>
+ <div id="container">
+ <div class="grid-tr">
+ <div id="header" class="grid-td">
+ mongoDB :: collection.storageDetails({analyze: 'pagesInRAM'}) command (mongod should be started with --rest --jsonp)
+ </div>
+ <div id="legend" class="grid-td">
+ <!--
+ <span class="col" style="background-color: #88f;"></span> bson
+ <span class="col" style="background-color: #bbf;"></span> padding+header
+ -->
+
+ </div>
+ </div>
+ <div class="grid-tr">
+ <div id="requestForm" class="grid-td">
+ </div>
+ </div>
+ <div class="grid-tr">
+ <div id="resultString" class="grid-td">
+ </div>
+ </div>
+ <div id="basicInfoRow" class="grid-tr">
+ </div>
+ <div id="extentsSummaryRow" class="grid-tr" style="display: none;">
+ <div class="grid-td left-table-header">
+ <span>summary</span>
+ </div>
+ <div id="extentsSummary" class="grid-td">
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript" src="pagesInRAM.js"></script>
+ </body>
+</html>
+
View
250 pagesInRAM.js
@@ -0,0 +1,250 @@
+// index.js
+
+/**
+* Copyright (C) 2012 10gen Inc.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License, version 3,
+* as published by the Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+(function() {
+
+var URL_TEMPLATE = "http://<%=host%>/<%=database%>/$cmd/?filter_storageDetails=<%=collection%>" +
+ "&filter_analyze=pagesInRAM" +
+ // "<%=(extent) ? '&filter_extent=' + extent : ''%>" +
+ "<%=(granularity) ? '&filter_granularity=' + (granularity * 1024) : ''%>" +
+ "<%=(numberOfSlices) ? '&filter_numberOfSlices=' + numberOfSlices : ''%>";
+
+var REQUEST_FORM_FIELDS = [
+ { name: 'host', desc: 'host', type: 'text', default_: 'localhost:28017' },
+ { name: 'database', desc: 'db', type: 'text', default_: 'test' },
+ { name: 'collection', desc: 'collection', type: 'text', default_: 'test' },
+ // { name: 'extent', desc: 'extent (opt)', type: 'text', default_: '' },
+ { name: 'granularity', desc: 'granularity (Kb) (opt)', type: 'text', default_: '20' },
+ { name: 'numberOfSlices', desc: 'number of slices (opt)', type: 'text', default_: '' }
+]
+
+function layoutHacks() {
+ d3.selectAll('.slicesGraph')
+ .style('max-width', window.document.documentElement.clientWidth - 150);
+}
+
+function setUp() {
+ var requestForm = d3.select('#requestForm');
+ base.generateFormFields(requestForm, REQUEST_FORM_FIELDS, function() {
+ var reqParams = base.collectFormValues(requestForm, REQUEST_FORM_FIELDS);
+ console.log(reqParams);
+ var url = base.tmpl(URL_TEMPLATE, reqParams);
+ d3.select('#resultString').text('fetching ' + url + '...');
+ base.jsonp(url, 'handleData');
+ });
+
+ d3.select(window).on('resize', layoutHacks);
+}
+
+this.handleData = function handleData(data) {
+ _data = data.rows[0];
+ if (!_data.ok) {
+ d3.select('#resultString').text('error: ' + _data.errmsg);
+ return;
+ }
+ d3.select('#resultString').text('executed command with params ' + JSON.stringify(data.query) +
+ ', rendering');
+ console.log(_data);
+
+ d3.select('#resultString').text('executed command ' + JSON.stringify(data.query));
+
+ var $extentsSummaryRow = d3.select('#extentsSummaryRow').style('display', 'none');
+ var $extentsSummary = d3.select('#extentsSummary');
+ $extentsSummary.selectAll('*').remove();
+
+ var basicInfoRow = d3.select('#basicInfoRow');
+ basicInfoRow.selectAll('*').remove();
+ d3.selectAll('.extentRow').remove();
+
+ var SUMMARY_BAR_WIDTH = 20;
+ var BAR_HEIGHT = 70;
+ var BAR_WIDTH = 4;
+
+ if (_data.extents) {
+ $extentsSummaryRow.style('display', null);
+
+ basicInfoRow.append('div')
+ .classed('grid-td', true)
+ .html('page size: ' + base.fmt.suffix(_data.extents[0].pageBytes));
+ basicInfoRow.append('div')
+ .classed('grid-td', true)
+ .html('each bar in summary refers to the ratio of pages in ram for an extent, ' +
+ 'click on them to find the detailed view for the extent');
+
+
+ var summaryX = function(d, i) { return (SUMMARY_BAR_WIDTH + 10) * i };
+
+ $extentsSummary.append('svg')
+ .attr('width', summaryX(null, _data.extents.length) + 1)
+ .attr('height', BAR_HEIGHT + 20)
+ .selectAll('g.extent')
+ .data(_data.extents)
+ .enter()
+ .append('svg:g')
+ .attr('width', SUMMARY_BAR_WIDTH)
+ .attr('height', BAR_HEIGHT)
+ .classed('extent', true)
+ .attr('transform', function(d, i) { return 'translate(' + summaryX(d, i) + ', 0)' })
+ .call(summaryBar().width(SUMMARY_BAR_WIDTH).height(BAR_HEIGHT))
+ .on('click', function(datum, i) {
+ d3.selectAll('.extentRow')
+ .classed('highlighted', false);
+ d3.select('.extentRow-' + i)
+ .classed('highlighted', true)
+ .node().scrollIntoView();
+ })
+ .append('svg:text')
+ .attr('x', SUMMARY_BAR_WIDTH / 2)
+ .attr('y', BAR_HEIGHT + 15)
+ .style('text-anchor', 'middle')
+ .text(function(d, i) { return '' + (i + 1) });
+
+ var extentRowEnter = d3.select('#container')
+ .selectAll('.extentRow')
+ .data(_data.extents)
+ .enter()
+ .append('div')
+ .attr('class', function(d, i) { return 'extentRow-' + i })
+ .classed('grid-tr extentRow', true);
+
+ extentRowEnter.append('div')
+ .classed('grid-td left-table-header', true)
+ .append('span')
+ .text(function(d, i) { return 'extent ' + (i + 1) });
+
+ var extentChartDiv = extentRowEnter.append('div')
+ .classed('grid-td extentChart', true);
+
+ extentChartDiv.append('div')
+ .classed('extentSize', true)
+ .text(function(d, i) { return base.fmt.suffix(d.onDiskBytes) });
+
+ extentChartDiv.append('svg')
+ .attr('width', SUMMARY_BAR_WIDTH + 1)
+ .attr('height', BAR_HEIGHT + 1)
+ .call(summaryBar().width(SUMMARY_BAR_WIDTH).height(BAR_HEIGHT));
+
+ extentChartDiv.append('div')
+ .classed('extentPercent', true)
+ .text(function(d, i) { return base.fmt.ratioToPercent(d.inMem) });
+
+ extentRowEnter.append('div')
+ .classed('grid-td slicesGraph', true)
+ .append('svg')
+ .attr('height', BAR_HEIGHT + 30)
+ .attr('width', function(d) {
+ return d.slices ? d.slices.length * BAR_WIDTH + 20 : 0
+ })
+ .append('g')
+ .attr('transform', 'translate(10, 0)')
+ .call(slicesInRAMPlot().sliceWidth(BAR_WIDTH).height(BAR_HEIGHT));
+
+
+ } else {
+ d3.select('#resultString').text('single extent mode is not supported yet');
+ }
+
+ layoutHacks();
+};
+
+function summaryBar() {
+
+ base.property(chart, 'width', 20);
+ base.property(chart, 'height', 80);
+
+ function chart(g) {
+ g.each(function(data) {
+ var g = d3.select(this);
+
+ g.append('rect')
+ .classed('border', true)
+ .attr('width', chart._width)
+ .attr('height', chart._height);
+
+ var summaryY = d3.scale.linear().domain([0, 1]).range([chart._height, 0]);
+ var summaryHeight = summaryY.copy().range([0, chart._height]);
+
+ g.selectAll('rect.inRAM')
+ .data(function(d, i) { return [d.inMem] })
+ .enter()
+ .append('rect')
+ .classed('inRAM', true)
+ .attr('width', chart._width)
+ .attr('height', summaryHeight)
+ .attr('y', summaryY);
+ });
+ }
+
+ return chart;
+}
+
+function slicesInRAMPlot() {
+
+ base.property(chart, 'sliceWidth', 2);
+ base.property(chart, 'height', 80);
+
+ function chart(g) {
+ g.each(function(data) {
+ var g = d3.select(this);
+
+ if (!data.slices) {
+ return;
+ }
+
+ var length = data.slices.length;
+
+ var x = d3.scale.linear().domain([0, length]).range([0, length * chart._sliceWidth]);
+
+ var xAxis = d3.svg.axis()
+ .scale(x)
+ .tickValues(d3.range(0, data.slices.length, 16))
+ .tickSubdivide(1)
+ .tickFormat(function(d) { return base.fmt.suffix(d * data.sliceBytes) })
+ .orient('bottom');
+
+ g.append('g')
+ .attr('transform', 'translate(0, ' + (chart._height) + ')')
+ .classed('x axis', true)
+ .call(xAxis);
+
+ var y = d3.scale.linear().domain([0, 1]).range([chart._height, 0]);
+ var height = y.copy().range([0, chart._height]);
+
+ g.selectAll('rect.slice')
+ .data(function(d, i) { return d.slices })
+ .enter()
+ .append('rect')
+ .classed('slice', true)
+ .attr('x', function(d, i) { return x(i) })
+ .attr('width', chart._sliceWidth)
+ .attr('y', y)
+ .attr('height', height);
+
+ g.append('svg:path')
+ .classed('frame', true)
+ .attr('d', 'M0,' + chart._height + 'V0' + 'h' + x(length) + 'v' + chart._height);
+
+ });
+ }
+
+ return chart;
+}
+
+setUp();
+
+})();
View
1,059 underscore.js
@@ -0,0 +1,1059 @@
+// Underscore.js 1.3.3
+// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
+// Underscore may be freely distributed under the MIT license.
+// Portions of Underscore are inspired or borrowed from Prototype,
+// Oliver Steele's Functional, and John Resig's Micro-Templating.
+// For all details and documentation:
+// http://documentcloud.github.com/underscore
+
+(function() {
+
+ // Baseline setup
+ // --------------
+
+ // Establish the root object, `window` in the browser, or `global` on the server.
+ var root = this;
+
+ // Save the previous value of the `_` variable.
+ var previousUnderscore = root._;
+
+ // Establish the object that gets returned to break out of a loop iteration.
+ var breaker = {};
+
+ // Save bytes in the minified (but not gzipped) version:
+ var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+ // Create quick reference variables for speed access to core prototypes.
+ var slice = ArrayProto.slice,
+ unshift = ArrayProto.unshift,
+ toString = ObjProto.toString,
+ hasOwnProperty = ObjProto.hasOwnProperty;
+
+ // All **ECMAScript 5** native function implementations that we hope to use
+ // are declared here.
+ var
+ nativeForEach = ArrayProto.forEach,
+ nativeMap = ArrayProto.map,
+ nativeReduce = ArrayProto.reduce,
+ nativeReduceRight = ArrayProto.reduceRight,
+ nativeFilter = ArrayProto.filter,
+ nativeEvery = ArrayProto.every,
+ nativeSome = ArrayProto.some,
+ nativeIndexOf = ArrayProto.indexOf,
+ nativeLastIndexOf = ArrayProto.lastIndexOf,
+ nativeIsArray = Array.isArray,
+ nativeKeys = Object.keys,
+ nativeBind = FuncProto.bind;
+
+ // Create a safe reference to the Underscore object for use below.
+ var _ = function(obj) { return new wrapper(obj); };
+
+ // Export the Underscore object for **Node.js**, with
+ // backwards-compatibility for the old `require()` API. If we're in
+ // the browser, add `_` as a global object via a string identifier,
+ // for Closure Compiler "advanced" mode.
+ if (typeof exports !== 'undefined') {
+ if (typeof module !== 'undefined' && module.exports) {
+ exports = module.exports = _;
+ }
+ exports._ = _;
+ } else {
+ root['_'] = _;
+ }
+
+ // Current version.
+ _.VERSION = '1.3.3';
+
+ // Collection Functions
+ // --------------------
+
+ // The cornerstone, an `each` implementation, aka `forEach`.
+ // Handles objects with the built-in `forEach`, arrays, and raw objects.
+ // Delegates to **ECMAScript 5**'s native `forEach` if available.
+ var each = _.each = _.forEach = function(obj, iterator, context) {
+ if (obj == null) return;
+ if (nativeForEach && obj.forEach === nativeForEach) {
+ obj.forEach(iterator, context);
+ } else if (obj.length === +obj.length) {
+ for (var i = 0, l = obj.length; i < l; i++) {
+ if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
+ }
+ } else {
+ for (var key in obj) {
+ if (_.has(obj, key)) {
+ if (iterator.call(context, obj[key], key, obj) === breaker) return;
+ }
+ }
+ }
+ };
+
+ // Return the results of applying the iterator to each element.
+ // Delegates to **ECMAScript 5**'s native `map` if available.
+ _.map = _.collect = function(obj, iterator, context) {
+ var results = [];
+ if (obj == null) return results;
+ if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+ each(obj, function(value, index, list) {
+ results[results.length] = iterator.call(context, value, index, list);
+ });
+ if (obj.length === +obj.length) results.length = obj.length;
+ return results;
+ };
+
+ // **Reduce** builds up a single result from a list of values, aka `inject`,
+ // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+ _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+ var initial = arguments.length > 2;
+ if (obj == null) obj = [];
+ if (nativeReduce && obj.reduce === nativeReduce) {
+ if (context) iterator = _.bind(iterator, context);
+ return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+ }
+ each(obj, function(value, index, list) {
+ if (!initial) {