提交 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;
newIndices[sampledIndex++] = a; for (let chunkIdx = 0; chunkIdx < this._chunkCount; chunkIdx++) {
for (let i = 0; i < threshold - 2; i++) { 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];
// The first frame is the first data.
newIndices[sampledIndex++] = currentSelectedIdx;
for (let frame = 0; frame < chunkFrameCount - 2; frame++) {
let avgX = 0;
let avgY = 0;
let avgRangeStart = (frame + 1) * frameSize + 1 + chunkOffset;
const avgRangeEnd = Math.min((frame + 2) * frameSize + 1, selfChunkSize) + chunkOffset;
const avgRangeLength = avgRangeEnd - avgRangeStart;
for (; avgRangeStart < avgRangeEnd; avgRangeStart++) {
const x = baseDimChunk[avgRangeStart] as number;
const y = valueDimChunk[avgRangeStart] as number;
if (isNaN(x) || isNaN(y)) {
continue;
}
avgX += x;
avgY += y;
}
avgX /= avgRangeLength;
avgY /= avgRangeLength;
let avgX = 0; // Get the range for this bucket
let avgY = 0; let rangeOffs = (frame) * frameSize + 1 + chunkOffset;
let avgRangeStart = mathFloor((i + 1) * every) + 1; const rangeTo = (frame + 1) * frameSize + 1 + chunkOffset;
let avgRangeEnd = mathFloor((i + 2) * every) + 1;
avgRangeEnd = avgRangeEnd < len ? avgRangeEnd : len; // Point A
const pointAX = baseDimChunk[currentSelectedIdx] as number;
const pointAY = valueDimChunk[currentSelectedIdx] as number;
let allNaN = true;
const avgRangeLength = avgRangeEnd - avgRangeStart; maxArea = area = -1;
for (; avgRangeStart < avgRangeEnd; avgRangeStart++) { for (; rangeOffs < rangeTo; rangeOffs++) {
avgX += getPair(avgRangeStart)[0] * 1; // * 1 enforces Number (value may be Date) const y = valueDimChunk[rangeOffs] as number;
avgY += getPair(avgRangeStart)[1] * 1; const x = baseDimChunk[rangeOffs] as number;
} if (isNaN(x) || isNaN(y)) {
avgX /= avgRangeLength; continue;
avgY /= avgRangeLength; }
allNaN = false;
// Get the range for this bucket // Calculate triangle area over three buckets
let rangeOffs = mathFloor((i + 0) * every) + 1; area = Math.abs((pointAX - avgX) * (y - pointAY)
const rangeTo = mathFloor((i + 1) * every) + 1; - (pointAX - x) * (avgY - pointAY)
);
// Point a if (area > maxArea) {
const pointAX = getPair(a)[0] * 1; // enforce Number (value may be Date) maxArea = area;
const pointAY = getPair(a)[1] * 1; nextSelectedIdx = rangeOffs; // Next a is this b
}
maxArea = area = -1;
for (; rangeOffs < rangeTo; rangeOffs++) {
// Calculate triangle area over three buckets
area = Math.abs((pointAX - avgX) * (getPair(rangeOffs)[1] - pointAY)
- (pointAX - getPair(rangeOffs)[0]) * (avgY - pointAY)
) * 0.5;
if (area > maxArea) {
maxArea = area;
nextA = 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,162 +20,150 @@ under the License. ...@@ -20,162 +20,150 @@ 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'
// 'echarts/chart/sankey', // 'echarts/chart/sankey',
// 'echarts/component/tooltip' // 'echarts/component/tooltip'
], function (echarts) { ], function (echarts) {
function round2(val) { function round2(val) {
return Math.round(val * 100) / 100; return Math.round(val * 100) / 100;
} }
function round3(val) { function round3(val) {
return Math.round(val * 1000) / 1000; return Math.round(val * 1000) / 1000;
} }
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;
for (let i = 0, j = 0; i < packed.length; i += numFields, j++) { var data = new Float64Array((packed.length / numFields) * 4 * repeatTimes);
let date = packed[i] * 60 * 1000;
cpu[j] = [date, round3(100 - packed[i+1])]; var off = 0;
} var date = packed[0];
for (let repeat = 0; repeat < repeatTimes; repeat++) {
// console.timeEnd('prep'); for (let i = 0, j = 0; i < packed.length; i += numFields, j++) {
date += 1;
return [cpu]; data[off++] = date * 60 * 1000;
} data[off++] = round3(100 - packed[i + 1]);
data[off++] = round2(
function makeChart(data) { (100 * packed[i + 5]) / (packed[i + 5] + packed[i + 6])
console.time('chart'); );
data[off++] = packed[i + 3];
var dom = document.getElementById("container"); }
var myChart = echarts.init(dom); }
console.timeEnd('prep');
let opts = {
grid: { return data;
left: 40, }
top: 0,
right: 0, function makeChart(data) {
bottom: 30, var dom = document.getElementById('container');
}, var myChart = echarts.init(dom);
xAxis: {
type: 'time', let opts = {
splitLine: { animation: false,
show: false dataset: {
}, source: data,
data: data[0], dimensions: ['date', 'cpu', 'ram', 'tcpout']
}, },
yAxis: { tooltip: {
type: 'value' trigger: 'axis'
}, },
legend: { legend: {},
}, grid: {
series: [ containLabel: true,
{ left: 0,
name: 'none', top: 50,
type: 'line', right: 0,
showSymbol: false, bottom: 30
hoverAnimation: false, },
data: data[0], xAxis: {
lineStyle: { type: 'time'
normal: { },
opacity: 0.5, yAxis: [{
width: 1 type: 'value',
} max: 100,
} axisLabel: {
}, formatter: '{value} %'
{ }
name: 'lttb', }, {
type: 'line', type: 'value',
showSymbol: false, max: 100,
hoverAnimation: false, axisLabel: {
data: data[0], formatter: '{value} MB'
sampling: 'lttb', }
lineStyle: { }],
normal: { series: [{
opacity: 0.5, name: 'CPU',
width: 1 type: 'line',
} showSymbol: false,
} sampling: 'lttb',
}, lineStyle: { width: 1 },
{ emphasis: { lineStyle: { width: 1 } },
name: 'average', encode: {
type: 'line', x: 'date',
showSymbol: false, y: 'cpu'
hoverAnimation: false, }
data: data[0], }, {
sampling: 'average', name: 'RAM',
lineStyle: { type: 'line',
normal: { yAxisIndex: 1,
opacity: 0.5, showSymbol: false,
width: 1 sampling: 'lttb',
} lineStyle: { width: 1 },
} emphasis: { lineStyle: { width: 1 } },
}, encode: {
{ x: 'date',
name: 'max', y: 'ram'
type: 'line', }
showSymbol: false, }, {
hoverAnimation: false, name: 'TCP Out',
data: data[0], type: 'line',
sampling: 'max', yAxisIndex: 1,
lineStyle: { showSymbol: false,
normal: { sampling: 'lttb',
opacity: 0.5, lineStyle: { width: 1 },
width: 1 emphasis: { lineStyle: { width: 1 } },
} encode: {
} x: 'date',
}, y: 'tcpout'
{ }
name: 'min', }]
type: 'line', };
showSymbol: false, const startTime = performance.now();
hoverAnimation: false, myChart.setOption(opts, true);
data: data[0], const endTime = performance.now();
sampling: 'min', wait.textContent = 'Done! ' + (endTime - startTime).toFixed(0) + 'ms';
lineStyle: { }
normal: {
opacity: 0.5, let wait = document.getElementById('wait');
width: 1 wait.textContent = 'Fetching data.json (2.07MB)....';
} fetch('data/large-data.json')
} .then(r => r.json())
}, .then(packed => {
] wait.textContent = 'Rendering...';
}; let data = prepData(packed);
setTimeout(() => makeChart(data), 200);
myChart.setOption(opts, true); });
wait.textContent = "Done!";
console.timeEnd('chart');
}
let wait = document.getElementById("wait");
wait.textContent = "Fetching data.json (2.07MB)....";
fetch("./data/large-data.json").then(r => r.json()).then(packed => {
wait.textContent = "Rendering...";
let data = prepData(packed);
setTimeout(() => makeChart(data), 0);
});
}); });
</script> </script>
</body> </body>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册