淺談基準測試
在一個應用中,應前端要求需要過濾后端接口響應JSON數據中的 null 字段,過濾操作會有性能影響,那么如何決定是否增加這個功能呢?
首先需要確定衡量指標。通常時間(time)和空間(memory)是兩個衡量程序性能狀況額指標,在這個例子中空間并不是制約因素,因而只考慮時間指標。
實現
接著我們需要一個程序實現。這個實現簡單的遞歸過濾 Object 中值為 null 的字段,
/**
- 不過濾數組元素為null的情況,如
[null, 'foo', null]
過濾后仍然為[null, 'foo', null]
*/
function prune(data) {
if (_.isArray(data)) {
_.each(data, prune)
} else if (_.isObject(data)) {
_.each(data, function(value, key) {
if (_.isObject(value)) {
prune(value)
} else if (value === null) {
delete data[key]
}
})
}
return data
}</pre> 單元測試見附錄。
影響因素
然后根據程序實現判斷性能的影響因素
什么因素會影響時間指標呢?JSON數據的大小(size)?JSON數據的字段數?JSON數據的層次結構?
時間指標受JSON數據的字段(包括遞歸字段)影響,因為在 prune 的實現中,遍歷 Object 和 Array 的時間決定了程序執行時間。
benchmark
最后根據影響因素選擇測試數據,進行基準測試并得出結論
借助 benchmark.js ,以 noop 為參照組進行基準測試
有兩組測試數據,真實線上接口獲取的 realSamples 和隨機生成的模擬數據 fakeSamples 。
var fs = require('fs')
var Benchmark = require('benchmark')
var suite = new Benchmark.Suite()
var getSample = require('./sample').getSample
var getSampleSize = require('./sample').getSampleSize
var prune = require('../src/utility').prune
var noop = function(){}
var filenames = fs.readdirSync(dirname + '/samples')
var realSamples = filenames
.map(function(filename) {
return JSON.parse(
fs.readFileSync(dirname + /samples/${filename}
, 'utf8')
)
})
var realSizes = realSamples.map(getSampleSize)
var fakeSizes = [10, 100, 1000, 10000]
var fakeSamples = fakeSizes.map(function(size) {
return getSample(size)
})
// add tests
realSamples.forEach(function(sample, index) {
var filename = filenames[index]
suite
.add(prune#real:${filename}:${getSampleSize(sample)}
, function() {
prune(sample)
})
})
fakeSamples.forEach(function(sample, index) {
var size = fakeSizes[index]
suite
.add(prune#fake:${size}:${getSampleSize(sample)}
, function() {
prune(sample)
})
})
suite
.add('noop', function() {
noop(realSamples[0])
})
// add listeners
suite
.on('cycle', function(event) {
console.log(String(event.target))
})
.on('complete', function() {
var totalSize = realSizes.reduce(function(sum, size) {
return sum + size
}, 0)
var averageSize = Math.floor(totalSize / realSizes.length)
console.log(real samples total size ${totalSize}, average size ${averageSize}
)
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
// run
.run()</pre>
這里還實現了 getSampleSize 方法(見附錄),用于統計JSON數據的字段總量。以此來粗略估計線上真實接口返回數據的平均字段數量。
運行結果 [1]
prune#real:adverts.json:47 x 179,918 ops/sec ±2.10% (79 runs sampled)
prune#real:areas.json:5126 x 1,333 ops/sec ±2.47% (74 runs sampled)
prune#real:citys.json:6417 x 1,363 ops/sec ±1.07% (90 runs sampled)
prune#real:count.json:1037 x 6,043 ops/sec ±1.75% (89 runs sampled)
prune#real:menus.json:55 x 47,010 ops/sec ±1.34% (86 runs sampled)
prune#real:pois.json:3136 x 2,316 ops/sec ±3.16% (84 runs sampled)
prune#real:subway.json:1999 x 3,043 ops/sec ±1.85% (89 runs sampled)
prune#fake:10:10 x 286,702 ops/sec ±1.64% (88 runs sampled)
prune#fake:100:100 x 63,893 ops/sec ±1.56% (89 runs sampled)
prune#fake:1000:985 x 8,173 ops/sec ±1.63% (86 runs sampled)
prune#fake:10000:9995 x 997 ops/sec ±1.80% (87 runs sampled)
noop x 80,713,438 ops/sec ±1.85% (87 runs sampled)
real samples total size 17817, average size 2545
Fastest is noop
結論:平均字段總量為 2545 ,向上取證以 10000 量級計算,使用 prune 處理數據大約需要1ms,并不影響整個應用的性能。
小結
上面已經用黑體標記了重點,這里再做一次小結
- 確定衡量指標
- 實現程序
- 判斷影響因素
- 選擇測試數據,進行基準測試
- 得出結論
</ol>
附錄
- 看起來 getSampleSize 或 getSample 函數計算有偏差,不過在這里可以忽略這個問題。
</ol>
sample生成器
var Chance = require('chance')
var _ = require('lodash')
var DATA_TYPES = [
'bool',
'character',
'floating',
'integer',
'natural',
'string',
'Array',
'Object',
]
function getSample(size, sample, chance) {
chance = chance || new Chance()
sample = sample || {}
var index, cursor, pick, type, key, value
for (index=0, cursor=0; index<size; index++, cursor++) {
pick = chance.integer({min: index, max: size-1})
switch(type = chance.pick(DATA_TYPES)) {
case 'Array':
value = getSample(pick - index, [], chance)
index = pick
break
case 'Object':
value = getSample(pick - index, {}, chance)
index = pick
break
default:
value = chancetype
}
key = sample.constructor.name === 'Array' ? cursor : chance.word()
sample[key] = value
}
return sample
}
function getSampleSize(sample) {
return .reduce(sample, function(sum, value, key) {
if (.isArray(value)) {
sum += getSampleSize(value)
} else if (_.isObject(value)) {
sum += getSampleSize(value)
}
return sum + 1
}, 0)
}
module.exports = {
getSample: getSample,
getSampleSize: getSampleSize,
}</pre>
單元測試
describe('utility', () => {
describe('prune', () => {
it('do not touch primitive type', () => {
expect(prune(123)).to.deep.equal(123)
expect(prune('123')).to.deep.equal('123')
expect(prune(null)).to.deep.equal(null)
expect(prune([1, 2, '3'])).to.deep.equal([1, 2, '3'])
expect(prune({foo: 'bar'})).to.deep.equal({foo: 'bar'})
})
it('prune null value in object', () => {
expect(prune({foo: 'bar', baz: null})).to.deep.equal({foo: 'bar'})
})
it('do not prune null in array', () => {
expect(
prune([null, 'foo', null, 'bar', null])
).to.deep.equal([null, 'foo', null, 'bar', null])
})
it('complex json prune', () => {
expect(
prune([
null,
{
'foo1': 'bar1',
'foo2': {
'foo3': ['bar3', null],
'foo': null,
},
'foo': null
},
null
])
).to.deep.equal([
null,
{
'foo1': 'bar1',
'foo2': {
'foo3': ['bar3', null],
},
},
null
])
})
})
})</pre></article>
來自: https://cattail.me/tech/2016/01/12/how-to-benchmark.html
</code></code></code></code></code>