问题描述 目前在做的小项目中,我们在浏览器中通过Hook浏览器的API,截获目标程序对浏览器API的调用。比如对于Webgl的网页程序,我们就能截获所有的gl指令,这其中主要是截获参数列表。把这些gl指令翻译为Opengl ES标准的话便可以编译为一个其他设备上的原生应用了。
图形的绘制过程中经常会有大量的数组需要传递入GPU,这就造成了我们截获了大量的数组。如果想直接把这些数据保存在浏览器内存中,压力很大,实时流到其他服务器上的话也带来了stringfy瓶颈和传输瓶颈。虽然对于Javascript中的Typed Array,我们现在直接二进制传输到服务器上,是否能只传输变化的数组,或者是只传输数组的delta,这个是一个潜在的优化。当然,如果发现大部分要传输的数组都是改变过的话,或者是检测改变的开销大于直接传输的开销(毕竟是CPU时间对抗IO时间,而且由于检测比如会使目标程序运行变慢),完全便可以不考虑进行数组改变的检测了。
对于给定Javascript的TypedArray,我们的目标便是当截获到的gl指令需要使用它的时候,我们需要能知道它是否内容发生了改动,更进一步能记录出上次截获到这次截获间的delta。 花了一些时间进行探索,然而最后还是无解,并也没有方法能占用资源尽量少地做到标记修改,记录如下。
暴力检测 这个方法不需要多说,不过检测的时间消耗太多,由于消耗了Webgl每帧的CPU时间,很容易造成CPU性能瓶颈,导致Fps下降。
Object.observe() / Array.observe() 最先查询到的便是两个内建的API,直接来看MDN上的使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var obj = { foo: 0 , bar: 1 }; Object.observe(obj, function (changes) { console.log(changes); }); obj.baz = 2 ; // [{name: 'baz' , object: <obj>, type: 'add' }] obj.foo = 'hello' ; // [{name: 'foo' , object: <obj>, type: 'update' , oldValue: 0 }] delete obj.baz;// [{name: 'baz' , object: <obj>, type: 'delete' , oldValue: 2 }] ```
一开始看到的时候感觉非常符合我的需求,并且还区分了改动的类型,比如update
、add
、delete
类型。 应该现在很多前端的工具就使用Observe便可以完全数据到显示的单向同步了。 于是尝试了一下在Typed Array上的操作:
1 2 3 4 5 6 7 8 9 10 var obj = new Int16Array([1 ,2 ,3 ]); Object.observe(obj, function(changes) { console.log (changes); }); obj[0 ] = 9 ; obj[0 ] = 9 ; obj.sort(); ```
不过后来看到这么一个解释:
The Object.observe() method is used for asynchronously observing the changes to an object. It provides a stream of changes in the order in which they occur.
说明变更事件是异步通知的,比如可以这样实验一下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var a = new Int16Array ([1 ,2 ,3 ]);a.isDirty = false ; Object .observe(a, function (changes ) { a.isDirty = true ; console .log('c' ); }); a[0 ]=9 ; console .log(a.isDirty);setTimeout('console.log(a.isDirty);' ,100 ); output: false c true `` `
这就带来了问题,由于Javascript执行是单线程的,可能在webgl程序刚修改完后便被我截获了这个数组,但修改通知没到,我就已经错误判断数组未改变了,由于我相当于是对每个gl命令的参数进行快照,无法在未来进行记录,如果停止我当前线程的,会带来严重的性能问题。
Object.defineProperty 这个方法可以为对象定义属性,并能对属性的访问进行详细的控制,最简单的话我们给定义get和set函数,举例说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 var arr = {};var shadow = [1 ,2 ,3 ];Object .defineProperty(arr, '0' , { get: function ( ) { console .log('get' ); return shadow['0' ]; }, set: function (v ) { console .log('set' ); shadow['0' ] = v; } }); `` `
通常对外暴露的get,set中要访问一个内部的变量,以防止递归访问属性的get或是set。 这个方法其实是多加入了一个proxy的对象。并且TypedArray并不能重写数字属性的get/set,只能通过一个proxy对象了。
一个简陋的实现,直接替换浏览器中提供的API:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 var _Float32Array = Float32Array ;Float32Array = function ( ) { var list = [ "BYTES_PER_ELEMENT" , "__defineGetter__" , "__defineSetter__" , "__lookupGetter__" , "__lookupSetter__" , "constructor" , "buffer" , "byteLength" , "byteOffset" , "entries" , "hasOwnProperty" , "isPrototypeOf" , "keys" , "length" , "propertyIsEnumerable" , "set" , "subarray" , "toLocaleString" , "toString" , "valueOf" , "values" , ] var a = new _Float32Array(arguments [0 ]); this .a = a; var b = this ; this .isDirty = false ; list.forEach(function (key ) { if (typeof (a[key]) == 'function' ) { b[key] = function ( ) { b.isDirty = true ; console .log('invoke' , key); return a[key].apply(a, arguments ); } }else { Object .defineProperty(b, key, { configurable: true , enumerable: true , get: function (i ) { return function ( ) { console .log('get member' ,i); return a[i]; } }(key), set: function (i ) { return function (v ) { b.isDirty = true ; console .log('set member' ,i); a[i] = v; } }(key) }); } }) this .constructor = a.constructor; for (var idx in a) { Object .defineProperty(b, idx, { configurable: true , enumerable: true , get: function (i ) { return function ( ) { console .log('get index' ,i); return a[i]; } }(idx), set: function (i ) { return function (v ) { console .log('set index' ,i); b.isDirty = true ; a[i] = v; } }(idx) }); } } >>a = new Float32Array ([1 ,2 ,3 ]) <<Float32Array {a: Float32Array [3 ], isDirty: false } >>a.toString() <<t.js:64 invoke toString <<"1,2,3" >>a[0 ]=1 <<t.js:100 set index 0 <<1 >>a[2 ] <<t.js:94 get index 2 <<3 >>a.constructor <<Float32Array () { [native code] } `` `
本做法应该还有以下局限:
性能 大规模下创建一个1000000的数组内存占用飙升,因为创建了太多的get/set函数。
1 2 3 4 5 6 7 8 9 10 11 ori = new _Float32Array(1000000 ) Float32Array[1000000 ] mod = new Float32Array(1000000 ) Float32Array {a: Float32Array[1000000 ], isDirty: false } console.time('a' );for (i=0 ;i<1000000 ;i++){var t = mod[i];t++;mod[i]=t;};console.timeEnd('a' ); VM741:2 a: 5313.409 ms console.time('a' );for (i=0 ;i<1000000 ;i++){var t=ori[i];t++;ori[i]=t;};console.timeEnd('a' ); VM743:2 a: 3884.074 ms ```
内部创建并返回的TypedArray无法Hook 比如类似subarray()
函数,返回值便是原生的TypedArray了,需要在Proxy对象中特殊处理一下。
在某些场景下不能完全替代原有的TypedArray 可以尽量模拟原生TypedArray的对外Api,不过还是有些场景可能照顾不到。
ES6 Proxy & Reflect Chrome中还没有实现,使用Firefox一试。对某对象加一层通用的代理,非常容易实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var ori = new Float32Array (1000000 );var obj = new Proxy (aa, { get: function (target, key, receiver ) { return Reflect .get(target, key, receiver); }, set: function (target, key, value, receiver ) { return Reflect .set(target, key, value, receiver); } }); console .time('a' );for (i=0 ;i<1000000 ;i++){var t = aa[i];t++;aa[i]=t;};console .timeEnd('a' );a: 计时器开始 a: 1738.62 ms console .time('a' );for (i=0 ;i<1000000 ;i++){var t = obj[i];t++;obj[i]=t;};console .timeEnd('a' );a: 计时器开始 a: 3226.16 ms `` `
如果还有更好的方法,欢迎讨论。