提交 d6f63003 编写于 作者: P pissang

feat(sample): optimize performance of lttb sampling

上级 e9164538
...@@ -1694,15 +1694,14 @@ class List< ...@@ -1694,15 +1694,14 @@ class List<
/** /**
* Large data down sampling using largest-triangle-three-buckets * Large data down sampling using largest-triangle-three-buckets
* https://github.com/pingec/downsample-lttb
* @param {string} baseDimension * @param {string} baseDimension
* @param {string} valueDimension * @param {string} valueDimension
* @param {number} threshold target counts * @param {number} rate
*/ */
lttbDownSample( lttbDownSample(
baseDimension: DimensionName, baseDimension: DimensionName,
valueDimension: DimensionName, valueDimension: DimensionName,
threshold: number rate: number
) { ) {
const list = cloneListForMapAndSample(this, [baseDimension, valueDimension]); const list = cloneListForMapAndSample(this, [baseDimension, valueDimension]);
const targetStorage = list._storage; const targetStorage = list._storage;
...@@ -1711,72 +1710,84 @@ class List< ...@@ -1711,72 +1710,84 @@ class List<
const len = this.count(); const len = this.count();
const chunkSize = this._chunkSize; const chunkSize = this._chunkSize;
const newIndices = new (getIndicesCtor(this))(len); const newIndices = new (getIndicesCtor(this))(len);
const getPair = (
i: number
) : Array<any> => {
const originalChunkIndex = mathFloor(i / chunkSize);
const originalChunkOffset = i % chunkSize;
return [
baseDimStore[originalChunkIndex][originalChunkOffset],
valueDimStore[originalChunkIndex][originalChunkOffset]
];
};
let sampledIndex = 0; let sampledIndex = 0;
const every = (len - 2) / (threshold - 2); const frameSize = mathFloor(1 / rate);
let a = 0; let currentSelectedIdx = 0;
let maxArea; let maxArea;
let area; let area;
let nextA; let nextSelectedIdx;
for (let chunkIdx = 0; chunkIdx < this._chunkCount; chunkIdx++) {
const chunkOffset = chunkSize * chunkIdx;
const selfChunkSize = Math.min(len - chunkOffset, chunkSize);
const chunkFrameCount = Math.ceil((selfChunkSize - 2) / frameSize);
const baseDimChunk = baseDimStore[chunkIdx];
const valueDimChunk = valueDimStore[chunkIdx];
newIndices[sampledIndex++] = a; // The first frame is the first data.
for (let i = 0; i < threshold - 2; i++) { newIndices[sampledIndex++] = currentSelectedIdx;
for (let frame = 0; frame < chunkFrameCount - 2; frame++) {
let avgX = 0; let avgX = 0;
let avgY = 0; let avgY = 0;
let avgRangeStart = mathFloor((i + 1) * every) + 1; let avgRangeStart = (frame + 1) * frameSize + 1 + chunkOffset;
let avgRangeEnd = mathFloor((i + 2) * every) + 1; const avgRangeEnd = Math.min((frame + 2) * frameSize + 1, selfChunkSize) + chunkOffset;
avgRangeEnd = avgRangeEnd < len ? avgRangeEnd : len;
const avgRangeLength = avgRangeEnd - avgRangeStart; const avgRangeLength = avgRangeEnd - avgRangeStart;
for (; avgRangeStart < avgRangeEnd; avgRangeStart++) { for (; avgRangeStart < avgRangeEnd; avgRangeStart++) {
avgX += getPair(avgRangeStart)[0] * 1; // * 1 enforces Number (value may be Date) const x = baseDimChunk[avgRangeStart] as number;
avgY += getPair(avgRangeStart)[1] * 1; const y = valueDimChunk[avgRangeStart] as number;
if (isNaN(x) || isNaN(y)) {
continue;
}
avgX += x;
avgY += y;
} }
avgX /= avgRangeLength; avgX /= avgRangeLength;
avgY /= avgRangeLength; avgY /= avgRangeLength;
// Get the range for this bucket // Get the range for this bucket
let rangeOffs = mathFloor((i + 0) * every) + 1; let rangeOffs = (frame) * frameSize + 1 + chunkOffset;
const rangeTo = mathFloor((i + 1) * every) + 1; const rangeTo = (frame + 1) * frameSize + 1 + chunkOffset;
// Point a // Point A
const pointAX = getPair(a)[0] * 1; // enforce Number (value may be Date) const pointAX = baseDimChunk[currentSelectedIdx] as number;
const pointAY = getPair(a)[1] * 1; const pointAY = valueDimChunk[currentSelectedIdx] as number;
let allNaN = true;
maxArea = area = -1; maxArea = area = -1;
for (; rangeOffs < rangeTo; rangeOffs++) { for (; rangeOffs < rangeTo; rangeOffs++) {
const y = valueDimChunk[rangeOffs] as number;
const x = baseDimChunk[rangeOffs] as number;
if (isNaN(x) || isNaN(y)) {
continue;
}
allNaN = false;
// Calculate triangle area over three buckets // Calculate triangle area over three buckets
area = Math.abs((pointAX - avgX) * (getPair(rangeOffs)[1] - pointAY) area = Math.abs((pointAX - avgX) * (y - pointAY)
- (pointAX - getPair(rangeOffs)[0]) * (avgY - pointAY) - (pointAX - x) * (avgY - pointAY)
) * 0.5; );
if (area > maxArea) { if (area > maxArea) {
maxArea = area; maxArea = area;
nextA = rangeOffs; // Next a is this b nextSelectedIdx = rangeOffs; // Next a is this b
} }
} }
newIndices[sampledIndex++] = nextA; if (!allNaN) {
newIndices[sampledIndex++] = nextSelectedIdx;
}
a = nextA; // This a is the next a (chosen b) currentSelectedIdx = nextSelectedIdx; // This a is the next a (chosen b)
}
// The last frame is the last data.
newIndices[sampledIndex++] = selfChunkSize - 1;
} }
newIndices[sampledIndex++] = len - 1;
list._count = sampledIndex; list._count = sampledIndex;
list._indices = newIndices; list._indices = newIndices;
......
...@@ -95,7 +95,7 @@ export default function (seriesType: string): StageHandler { ...@@ -95,7 +95,7 @@ export default function (seriesType: string): StageHandler {
if (rate > 1) { if (rate > 1) {
if (sampling === 'lttb') { if (sampling === 'lttb') {
seriesModel.setData(data.lttbDownSample( seriesModel.setData(data.lttbDownSample(
data.mapDimension(baseAxis.dim), data.mapDimension(valueAxis.dim), size data.mapDimension(baseAxis.dim), data.mapDimension(valueAxis.dim), 1 / rate
)); ));
} }
let sampler; let sampler;
......
...@@ -20,17 +20,17 @@ under the License. ...@@ -20,17 +20,17 @@ under the License.
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset='utf-8'>
<title>ECharts Demo</title> <title>Downsample Comparasions</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name='viewport' content='width=device-width, initial-scale=1'>
</head> </head>
<body> <body>
<h2 id="wait">Loading lib....</h2> <h2 id='wait'>Loading lib....</h2>
<div id="container" style="height: 600px; width: 100%;"></div> <div id='container' style='height: 600px; width: 1200px;'></div>
<script src="lib/esl.js"></script> <script src='lib/esl.js'></script>
<script src="lib/config.js"></script> <script src='lib/config.js'></script>
<script> <script>
require([ require([
'echarts' 'echarts'
...@@ -46,135 +46,123 @@ under the License. ...@@ -46,135 +46,123 @@ under the License.
} }
function prepData(packed) { function prepData(packed) {
// console.time('prep'); console.time('prep');
// epoch,idl,recv,send,read,writ,used,free // epoch,idl,recv,send,read,writ,used,free
const numFields = packed[0]; var numFields = packed[0];
packed = packed.slice(numFields + 1); packed = packed.slice(numFields + 1);
var cpu = Array(packed.length/numFields); var repeatTimes = 1;
var data = new Float64Array((packed.length / numFields) * 4 * repeatTimes);
var off = 0;
var date = packed[0];
for (let repeat = 0; repeat < repeatTimes; repeat++) {
for (let i = 0, j = 0; i < packed.length; i += numFields, j++) { for (let i = 0, j = 0; i < packed.length; i += numFields, j++) {
let date = packed[i] * 60 * 1000; date += 1;
cpu[j] = [date, round3(100 - packed[i+1])]; data[off++] = date * 60 * 1000;
data[off++] = round3(100 - packed[i + 1]);
data[off++] = round2(
(100 * packed[i + 5]) / (packed[i + 5] + packed[i + 6])
);
data[off++] = packed[i + 3];
} }
}
console.timeEnd('prep');
// console.timeEnd('prep'); return data;
return [cpu];
} }
function makeChart(data) { function makeChart(data) {
console.time('chart'); var dom = document.getElementById('container');
var dom = document.getElementById("container");
var myChart = echarts.init(dom); var myChart = echarts.init(dom);
let opts = { let opts = {
animation: false,
dataset: {
source: data,
dimensions: ['date', 'cpu', 'ram', 'tcpout']
},
tooltip: {
trigger: 'axis'
},
legend: {},
grid: { grid: {
left: 40, containLabel: true,
top: 0, left: 0,
top: 50,
right: 0, right: 0,
bottom: 30, bottom: 30
}, },
xAxis: { xAxis: {
type: 'time', type: 'time'
splitLine: {
show: false
},
data: data[0],
}, },
yAxis: { yAxis: [{
type: 'value' type: 'value',
}, max: 100,
legend: { axisLabel: {
}, formatter: '{value} %'
series: [
{
name: 'none',
type: 'line',
showSymbol: false,
hoverAnimation: false,
data: data[0],
lineStyle: {
normal: {
opacity: 0.5,
width: 1
} }
}, {
type: 'value',
max: 100,
axisLabel: {
formatter: '{value} MB'
} }
}, }],
{ series: [{
name: 'lttb', name: 'CPU',
type: 'line', type: 'line',
showSymbol: false, showSymbol: false,
hoverAnimation: false,
data: data[0],
sampling: 'lttb', sampling: 'lttb',
lineStyle: { lineStyle: { width: 1 },
normal: { emphasis: { lineStyle: { width: 1 } },
opacity: 0.5, encode: {
width: 1 x: 'date',
} y: 'cpu'
} }
}, }, {
{ name: 'RAM',
name: 'average',
type: 'line', type: 'line',
yAxisIndex: 1,
showSymbol: false, showSymbol: false,
hoverAnimation: false, sampling: 'lttb',
data: data[0], lineStyle: { width: 1 },
sampling: 'average', emphasis: { lineStyle: { width: 1 } },
lineStyle: { encode: {
normal: { x: 'date',
opacity: 0.5, y: 'ram'
width: 1
}
} }
}, }, {
{ name: 'TCP Out',
name: 'max',
type: 'line', type: 'line',
yAxisIndex: 1,
showSymbol: false, showSymbol: false,
hoverAnimation: false, sampling: 'lttb',
data: data[0], lineStyle: { width: 1 },
sampling: 'max', emphasis: { lineStyle: { width: 1 } },
lineStyle: { encode: {
normal: { x: 'date',
opacity: 0.5, y: 'tcpout'
width: 1
}
}
},
{
name: 'min',
type: 'line',
showSymbol: false,
hoverAnimation: false,
data: data[0],
sampling: 'min',
lineStyle: {
normal: {
opacity: 0.5,
width: 1
}
} }
}, }]
]
}; };
const startTime = performance.now();
myChart.setOption(opts, true); myChart.setOption(opts, true);
const endTime = performance.now();
wait.textContent = "Done!"; wait.textContent = 'Done! ' + (endTime - startTime).toFixed(0) + 'ms';
console.timeEnd('chart');
} }
let wait = document.getElementById("wait"); let wait = document.getElementById('wait');
wait.textContent = "Fetching data.json (2.07MB)...."; wait.textContent = 'Fetching data.json (2.07MB)....';
fetch("./data/large-data.json").then(r => r.json()).then(packed => { fetch('data/large-data.json')
wait.textContent = "Rendering..."; .then(r => r.json())
.then(packed => {
wait.textContent = 'Rendering...';
let data = prepData(packed); let data = prepData(packed);
setTimeout(() => makeChart(data), 0); setTimeout(() => makeChart(data), 200);
}); });
}); });
</script> </script>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册