2011-10-05 13:40:49 +00:00
|
|
|
// Copyright 2011 the V8 project authors. All rights reserved.
|
|
|
|
// Redistribution and use in source and binary forms, with or without
|
|
|
|
// modification, are permitted provided that the following conditions are
|
|
|
|
// met:
|
|
|
|
//
|
|
|
|
// * Redistributions of source code must retain the above copyright
|
|
|
|
// notice, this list of conditions and the following disclaimer.
|
|
|
|
// * Redistributions in binary form must reproduce the above
|
|
|
|
// copyright notice, this list of conditions and the following
|
|
|
|
// disclaimer in the documentation and/or other materials provided
|
|
|
|
// with the distribution.
|
|
|
|
// * Neither the name of Google Inc. nor the names of its
|
|
|
|
// contributors may be used to endorse or promote products derived
|
|
|
|
// from this software without specific prior written permission.
|
|
|
|
//
|
|
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
|
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
|
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
|
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
|
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
|
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
|
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
|
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function provides requestAnimationFrame in a cross browser way.
|
|
|
|
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/
|
|
|
|
*/
|
|
|
|
if ( !window.requestAnimationFrame ) {
|
|
|
|
window.requestAnimationFrame = ( function() {
|
|
|
|
return window.webkitRequestAnimationFrame ||
|
|
|
|
window.mozRequestAnimationFrame ||
|
|
|
|
window.oRequestAnimationFrame ||
|
|
|
|
window.msRequestAnimationFrame ||
|
|
|
|
function(callback, element) {
|
|
|
|
window.setTimeout( callback, 1000 / 60 );
|
|
|
|
};
|
|
|
|
} )();
|
|
|
|
}
|
|
|
|
|
|
|
|
var kNPoints = 8000;
|
|
|
|
var kNModifications = 20;
|
|
|
|
var kNVisiblePoints = 200;
|
|
|
|
var kDecaySpeed = 20;
|
|
|
|
|
|
|
|
var kPointRadius = 4;
|
|
|
|
var kInitialLifeForce = 100;
|
|
|
|
|
|
|
|
var livePoints = void 0;
|
|
|
|
var dyingPoints = void 0;
|
|
|
|
var scene = void 0;
|
|
|
|
var renderingStartTime = void 0;
|
|
|
|
var scene = void 0;
|
|
|
|
var pausePlot = void 0;
|
|
|
|
var splayTree = void 0;
|
2011-10-31 17:05:37 +00:00
|
|
|
var numberOfFrames = 0;
|
|
|
|
var sumOfSquaredPauses = 0;
|
|
|
|
var benchmarkStartTime = void 0;
|
|
|
|
var benchmarkTimeLimit = void 0;
|
2011-11-15 22:52:38 +00:00
|
|
|
var autoScale = void 0;
|
2011-10-31 17:05:37 +00:00
|
|
|
var pauseDistribution = [];
|
2011-10-05 13:40:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
function Point(x, y, z, payload) {
|
|
|
|
this.x = x;
|
|
|
|
this.y = y;
|
|
|
|
this.z = z;
|
|
|
|
|
|
|
|
this.next = null;
|
|
|
|
this.prev = null;
|
|
|
|
this.payload = payload;
|
|
|
|
this.lifeForce = kInitialLifeForce;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Point.prototype.color = function () {
|
|
|
|
return "rgba(0, 0, 0, " + (this.lifeForce / kInitialLifeForce) + ")";
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Point.prototype.decay = function () {
|
|
|
|
this.lifeForce -= kDecaySpeed;
|
|
|
|
return this.lifeForce <= 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function PointsList() {
|
|
|
|
this.head = null;
|
|
|
|
this.count = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PointsList.prototype.add = function (point) {
|
|
|
|
if (this.head !== null) this.head.prev = point;
|
|
|
|
point.next = this.head;
|
|
|
|
this.head = point;
|
|
|
|
this.count++;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PointsList.prototype.remove = function (point) {
|
|
|
|
if (point.next !== null) {
|
|
|
|
point.next.prev = point.prev;
|
|
|
|
}
|
|
|
|
if (point.prev !== null) {
|
|
|
|
point.prev.next = point.next;
|
|
|
|
} else {
|
|
|
|
this.head = point.next;
|
|
|
|
}
|
|
|
|
this.count--;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function GeneratePayloadTree(depth, tag) {
|
|
|
|
if (depth == 0) {
|
|
|
|
return {
|
|
|
|
array : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
|
|
|
|
string : 'String for key ' + tag + ' in leaf node'
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
left: GeneratePayloadTree(depth - 1, tag),
|
|
|
|
right: GeneratePayloadTree(depth - 1, tag)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// To make the benchmark results predictable, we replace Math.random
|
|
|
|
// with a 100% deterministic alternative.
|
|
|
|
Math.random = (function() {
|
|
|
|
var seed = 49734321;
|
|
|
|
return function() {
|
|
|
|
// Robert Jenkins' 32 bit integer hash function.
|
|
|
|
seed = ((seed + 0x7ed55d16) + (seed << 12)) & 0xffffffff;
|
|
|
|
seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff;
|
|
|
|
seed = ((seed + 0x165667b1) + (seed << 5)) & 0xffffffff;
|
|
|
|
seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff;
|
|
|
|
seed = ((seed + 0xfd7046c5) + (seed << 3)) & 0xffffffff;
|
|
|
|
seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff;
|
|
|
|
return (seed & 0xfffffff) / 0x10000000;
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
function GenerateKey() {
|
|
|
|
// The benchmark framework guarantees that Math.random is
|
|
|
|
// deterministic; see base.js.
|
|
|
|
return Math.random();
|
|
|
|
}
|
|
|
|
|
|
|
|
function CreateNewPoint() {
|
|
|
|
// Insert new node with a unique key.
|
|
|
|
var key;
|
|
|
|
do { key = GenerateKey(); } while (splayTree.find(key) != null);
|
|
|
|
|
|
|
|
var point = new Point(Math.random() * 40 - 20,
|
|
|
|
Math.random() * 40 - 20,
|
|
|
|
Math.random() * 40 - 20,
|
|
|
|
GeneratePayloadTree(5, "" + key));
|
|
|
|
|
|
|
|
livePoints.add(point);
|
|
|
|
|
|
|
|
splayTree.insert(key, point);
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ModifyPointsSet() {
|
|
|
|
if (livePoints.count < kNPoints) {
|
|
|
|
for (var i = 0; i < kNModifications; i++) {
|
|
|
|
CreateNewPoint();
|
|
|
|
}
|
|
|
|
} else if (kNModifications === 20) {
|
|
|
|
kNModifications = 80;
|
|
|
|
kDecay = 30;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < kNModifications; i++) {
|
|
|
|
var key = CreateNewPoint();
|
|
|
|
var greatest = splayTree.findGreatestLessThan(key);
|
|
|
|
if (greatest == null) {
|
|
|
|
var point = splayTree.remove(key).value;
|
|
|
|
} else {
|
|
|
|
var point = splayTree.remove(greatest.key).value;
|
|
|
|
}
|
|
|
|
livePoints.remove(point);
|
|
|
|
point.payload = null;
|
|
|
|
dyingPoints.add(point);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
function PausePlot(width, height, size, scale) {
|
2011-10-05 13:40:49 +00:00
|
|
|
var canvas = document.createElement("canvas");
|
|
|
|
canvas.width = this.width = width;
|
|
|
|
canvas.height = this.height = height;
|
|
|
|
document.body.appendChild(canvas);
|
|
|
|
|
|
|
|
this.ctx = canvas.getContext('2d');
|
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
if (typeof scale !== "number") {
|
|
|
|
this.autoScale = true;
|
|
|
|
this.maxPause = 0;
|
|
|
|
} else {
|
|
|
|
this.autoScale = false;
|
|
|
|
this.maxPause = scale;
|
|
|
|
}
|
|
|
|
|
2011-10-05 13:40:49 +00:00
|
|
|
this.size = size;
|
|
|
|
|
|
|
|
// Initialize cyclic buffer for pauses.
|
|
|
|
this.pauses = new Array(this.size);
|
|
|
|
this.start = this.size;
|
|
|
|
this.idx = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PausePlot.prototype.addPause = function (p) {
|
|
|
|
if (this.idx === this.size) {
|
|
|
|
this.idx = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.idx === this.start) {
|
|
|
|
this.start++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.start === this.size) {
|
|
|
|
this.start = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pauses[this.idx++] = p;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
PausePlot.prototype.iteratePauses = function (f) {
|
|
|
|
if (this.start < this.idx) {
|
|
|
|
for (var i = this.start; i < this.idx; i++) {
|
|
|
|
f.call(this, i - this.start, this.pauses[i]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (var i = this.start; i < this.size; i++) {
|
|
|
|
f.call(this, i - this.start, this.pauses[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
var offs = this.size - this.start;
|
|
|
|
for (var i = 0; i < this.idx; i++) {
|
|
|
|
f.call(this, i + offs, this.pauses[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
PausePlot.prototype.draw = function () {
|
|
|
|
var first = null;
|
2011-11-15 22:52:38 +00:00
|
|
|
|
|
|
|
if (this.autoScale) {
|
|
|
|
this.iteratePauses(function (i, v) {
|
|
|
|
if (first === null) {
|
|
|
|
first = v;
|
|
|
|
}
|
|
|
|
this.maxPause = Math.max(v, this.maxPause);
|
|
|
|
});
|
|
|
|
}
|
2011-10-05 13:40:49 +00:00
|
|
|
|
|
|
|
var dx = this.width / this.size;
|
|
|
|
var dy = this.height / this.maxPause;
|
|
|
|
|
|
|
|
this.ctx.save();
|
2011-11-15 22:52:38 +00:00
|
|
|
this.ctx.clearRect(0, 0, this.width, this.height);
|
2011-10-05 13:40:49 +00:00
|
|
|
this.ctx.beginPath();
|
|
|
|
this.ctx.moveTo(1, dy * this.pauses[this.start]);
|
|
|
|
var p = first;
|
|
|
|
this.iteratePauses(function (i, v) {
|
|
|
|
var delta = v - p;
|
|
|
|
var x = 1 + dx * i;
|
|
|
|
var y = dy * v;
|
|
|
|
this.ctx.lineTo(x, y);
|
|
|
|
if (delta > 2 * (p / 3)) {
|
|
|
|
this.ctx.font = "bold 12px sans-serif";
|
|
|
|
this.ctx.textBaseline = "bottom";
|
|
|
|
this.ctx.fillText(v + "ms", x + 2, y);
|
|
|
|
}
|
|
|
|
p = v;
|
|
|
|
});
|
|
|
|
this.ctx.strokeStyle = "black";
|
|
|
|
this.ctx.stroke();
|
|
|
|
this.ctx.restore();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Scene(width, height) {
|
|
|
|
var canvas = document.createElement("canvas");
|
|
|
|
canvas.width = width;
|
|
|
|
canvas.height = height;
|
|
|
|
document.body.appendChild(canvas);
|
|
|
|
|
|
|
|
this.ctx = canvas.getContext('2d');
|
|
|
|
this.width = canvas.width;
|
|
|
|
this.height = canvas.height;
|
|
|
|
|
|
|
|
// Projection configuration.
|
|
|
|
this.x0 = canvas.width / 2;
|
|
|
|
this.y0 = canvas.height / 2;
|
|
|
|
this.z0 = 100;
|
|
|
|
this.f = 1000; // Focal length.
|
|
|
|
|
|
|
|
// Camera is rotating around y-axis.
|
|
|
|
this.angle = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Scene.prototype.drawPoint = function (x, y, z, color) {
|
|
|
|
// Rotate the camera around y-axis.
|
|
|
|
var rx = x * Math.cos(this.angle) - z * Math.sin(this.angle);
|
|
|
|
var ry = y;
|
|
|
|
var rz = x * Math.sin(this.angle) + z * Math.cos(this.angle);
|
|
|
|
|
|
|
|
// Perform perspective projection.
|
|
|
|
var px = (this.f * rx) / (rz - this.z0) + this.x0;
|
|
|
|
var py = (this.f * ry) / (rz - this.z0) + this.y0;
|
|
|
|
|
|
|
|
this.ctx.save();
|
|
|
|
this.ctx.fillStyle = color
|
|
|
|
this.ctx.beginPath();
|
|
|
|
this.ctx.arc(px, py, kPointRadius, 0, 2 * Math.PI, true);
|
|
|
|
this.ctx.fill();
|
|
|
|
this.ctx.restore();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Scene.prototype.drawDyingPoints = function () {
|
|
|
|
var point_next = null;
|
|
|
|
for (var point = dyingPoints.head; point !== null; point = point_next) {
|
|
|
|
// Rotate the scene around y-axis.
|
|
|
|
scene.drawPoint(point.x, point.y, point.z, point.color());
|
|
|
|
|
|
|
|
point_next = point.next;
|
|
|
|
|
|
|
|
// Decay the current point and remove it from the list
|
|
|
|
// if it's life-force ran out.
|
|
|
|
if (point.decay()) {
|
|
|
|
dyingPoints.remove(point);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Scene.prototype.draw = function () {
|
|
|
|
this.ctx.save();
|
|
|
|
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
|
|
this.drawDyingPoints();
|
|
|
|
this.ctx.restore();
|
|
|
|
|
|
|
|
this.angle += Math.PI / 90.0;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2011-10-31 17:05:37 +00:00
|
|
|
function updateStats(pause) {
|
|
|
|
numberOfFrames++;
|
|
|
|
if (pause > 20) {
|
|
|
|
sumOfSquaredPauses += (pause - 20) * (pause - 20);
|
|
|
|
}
|
2011-10-31 17:26:59 +00:00
|
|
|
pauseDistribution[Math.floor(pause / 10)] |= 0;
|
|
|
|
pauseDistribution[Math.floor(pause / 10)]++;
|
2011-10-31 17:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderStats() {
|
|
|
|
var msg = document.createElement("p");
|
|
|
|
msg.innerHTML = "Score " +
|
|
|
|
Math.round(numberOfFrames * 1000 / sumOfSquaredPauses);
|
|
|
|
var table = document.createElement("table");
|
|
|
|
table.align = "center";
|
|
|
|
for (var i = 0; i < pauseDistribution.length; i++) {
|
|
|
|
if (pauseDistribution[i] > 0) {
|
|
|
|
var row = document.createElement("tr");
|
|
|
|
var time = document.createElement("td");
|
|
|
|
var count = document.createElement("td");
|
|
|
|
time.innerHTML = i*10 + "-" + (i+1)*10 + "ms";
|
|
|
|
count.innerHTML = " => " + pauseDistribution[i];
|
|
|
|
row.appendChild(time);
|
|
|
|
row.appendChild(count);
|
|
|
|
table.appendChild(row);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
div.appendChild(msg);
|
|
|
|
div.appendChild(table);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-10-05 13:40:49 +00:00
|
|
|
function render() {
|
|
|
|
if (typeof renderingStartTime === 'undefined') {
|
|
|
|
renderingStartTime = Date.now();
|
2011-10-31 17:05:37 +00:00
|
|
|
benchmarkStartTime = renderingStartTime;
|
2011-10-05 13:40:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ModifyPointsSet();
|
|
|
|
|
|
|
|
scene.draw();
|
|
|
|
|
|
|
|
var renderingEndTime = Date.now();
|
2011-10-11 11:43:48 +00:00
|
|
|
var pause = renderingEndTime - renderingStartTime;
|
|
|
|
pausePlot.addPause(pause);
|
2011-10-05 13:40:49 +00:00
|
|
|
renderingStartTime = renderingEndTime;
|
|
|
|
|
|
|
|
pausePlot.draw();
|
|
|
|
|
2011-10-31 17:05:37 +00:00
|
|
|
updateStats(pause);
|
|
|
|
|
2011-10-05 13:40:49 +00:00
|
|
|
div.innerHTML =
|
|
|
|
livePoints.count + "/" + dyingPoints.count + " " +
|
2011-10-31 17:05:37 +00:00
|
|
|
pause + "(max = " + pausePlot.maxPause + ") ms " +
|
|
|
|
numberOfFrames + " frames";
|
|
|
|
|
|
|
|
if (renderingEndTime < benchmarkStartTime + benchmarkTimeLimit) {
|
|
|
|
// Schedule next frame.
|
|
|
|
requestAnimationFrame(render);
|
|
|
|
} else {
|
|
|
|
renderStats();
|
|
|
|
}
|
|
|
|
}
|
2011-10-05 13:40:49 +00:00
|
|
|
|
2011-10-31 17:05:37 +00:00
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
function Form() {
|
|
|
|
function create(tag) { return document.createElement(tag); }
|
|
|
|
function text(value) { return document.createTextNode(value); }
|
2011-11-16 01:44:17 +00:00
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
this.form = create("form");
|
|
|
|
this.form.setAttribute("action", "javascript:start()");
|
|
|
|
|
|
|
|
var table = create("table");
|
|
|
|
table.setAttribute("style", "margin-left: auto; margin-right: auto;");
|
|
|
|
|
|
|
|
function col(a) {
|
|
|
|
var td = create("td");
|
|
|
|
td.appendChild(a);
|
|
|
|
return td;
|
|
|
|
}
|
|
|
|
|
|
|
|
function row(a, b) {
|
|
|
|
var tr = create("tr");
|
|
|
|
tr.appendChild(col(a));
|
|
|
|
tr.appendChild(col(b));
|
2011-11-16 01:44:17 +00:00
|
|
|
return tr;
|
2011-11-15 22:52:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.timelimit = create("input");
|
|
|
|
this.timelimit.setAttribute("value", "60");
|
|
|
|
|
|
|
|
table.appendChild(row(text("Time limit in seconds"), this.timelimit));
|
|
|
|
|
|
|
|
this.autoscale = create("input");
|
|
|
|
this.autoscale.setAttribute("type", "checkbox");
|
|
|
|
this.autoscale.setAttribute("checked", "true");
|
|
|
|
table.appendChild(row(text("Autoscale pauses plot"), this.autoscale));
|
|
|
|
|
|
|
|
var button = create("input");
|
2011-10-31 17:05:37 +00:00
|
|
|
button.setAttribute("type", "submit");
|
|
|
|
button.setAttribute("value", "Start");
|
2011-11-15 22:52:38 +00:00
|
|
|
this.form.appendChild(table);
|
|
|
|
this.form.appendChild(button);
|
|
|
|
|
|
|
|
document.body.appendChild(this.form);
|
2011-10-05 13:40:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
Form.prototype.remove = function () {
|
|
|
|
document.body.removeChild(this.form);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2011-10-05 13:40:49 +00:00
|
|
|
function init() {
|
|
|
|
livePoints = new PointsList;
|
|
|
|
dyingPoints = new PointsList;
|
|
|
|
|
|
|
|
splayTree = new SplayTree();
|
|
|
|
|
|
|
|
scene = new Scene(640, 480);
|
|
|
|
|
|
|
|
div = document.createElement("div");
|
|
|
|
document.body.appendChild(div);
|
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
pausePlot = new PausePlot(480, autoScale ? 240 : 500, 160, autoScale ? void 0 : 500);
|
2011-10-05 13:40:49 +00:00
|
|
|
}
|
|
|
|
|
2011-10-31 17:05:37 +00:00
|
|
|
function start() {
|
2011-11-15 22:52:38 +00:00
|
|
|
benchmarkTimeLimit = form.timelimit.value * 1000;
|
|
|
|
autoScale = form.autoscale.checked;
|
|
|
|
form.remove();
|
2011-10-31 17:05:37 +00:00
|
|
|
init();
|
|
|
|
render();
|
|
|
|
}
|
2011-10-05 13:40:49 +00:00
|
|
|
|
2011-11-15 22:52:38 +00:00
|
|
|
var form = new Form();
|