en-us Mon, 07 Sep 2015 03:19:27 +0000 Mon, 07 Sep 2015 03:19:27 +0000 玩转OneAPM Browser Insight 性能指标 /javascript/2015/09/02/BrowserInsight/ Wed, 02 Sep 2015 00:00:00 +0000 /javascript/2015/09/02/BrowserInsight <p>&nbsp;&nbsp;&nbsp;&nbsp;近期OneAPM Browser Insight 在原有的五个指标(请求排队、网络、web应用程序、页面加载、资源下载)的基础上增加了5个指标(白屏时间,首屏时间,页面加载完成时间, 资源下载完成时间,整页时间)。<br> &nbsp;&nbsp;&nbsp;&nbsp;这10个指标分别都是什么意思?和我的网站又有什么关系?为什么TOPN页面会惊现平均时间为1分钟?是谁在拖后腿?<br> 这篇文章将解答大家心中的困惑。<br> <img src="/images/session/bi4.png" alt="ruby-prof"> <code><h3>性能指标不在多,找到适合自己业务的,作为判断标准才是最好的,本期主要带领大家找到适合自己业务性能指标</h3></code></p> <h2>用户打开一个页面的过程</h2> <p>从用户在浏览器地址栏里面输入一个网址,到用户最终看到页面,在页面上可以进行各种操作,简单分为一下几个过程<br> 1、用户输入网址,浏览器发出请求。(主要是网络耗时)<br> 2、web服务器接到请求,处理请求,(主要是web服务器耗时)<br> 3、web服务器返回数据,浏览器开始接受数据(主要是网络耗时)<br> 4、浏览器边接受数据边加载页面中的脚本(下载js脚本等耗时)<br> 5、浏览器下载页面中的样式、图片、视频等资源(下载图片等耗时)<br></p> <h2>OneApm Browser Insight 指标介绍</h2> <p>OneApm Browser Insight 10个指标从两个方面描述了网页加载过程<br> <pre> 第一个方面 页面加载5阶段的耗时,用于分片运营(请求排队、网络、web应用程序、页面加载、资源下载)<br> 第二个方面 页面加载过程中的5个时间点,分别对应用户感知网页加载过程中的5个关键时间点和页面状态<br> </pre> 分段运营的5个指标中,请求排队和web应用程序 是从Ai 中获取到的<br> 网络=总网络时间-web应用程序耗时<br> 页面加载时间段=页面加载完成时间-白屏时间<br> 资源下载时间段=资源下载完成时间-面加载完成时间<br> 在试用的过程中,当应用程序异常时候,web应用程序面积就会增大<br> 当网络异常时候,网络的面积就会增大<br> <pre> 平时大家看到页面加载时段或者资源下载时段面积大,是因为在一般情况下,web和网络都不是性能的瓶颈,性能瓶颈往往在复杂的前端页面和资源的加载过程<br> </pre> <img src="/images/session/bi1.png" alt="ruby-prof"></p> <h2>页面加载过程中5个关键的时间点</h2> <p>白屏时间:打开一个页面感觉屏幕从白色开始变化时刻<br> 首屏时间:页面刚开始加载是杂乱无章,无样式的(在网速缓慢时候特别明显),当页面样式加载完毕的时候,页面就好看多了<br></p> <p>页面加载完成时间:<pre> 页面加载完成时间是和业务关系最为密切的时间点,大量js业务逻辑都在这个时间点触发<br> 如果你发现页面上一个按钮开始无法点击,过一会可以点击了,那么可以点击的时间点就是页面加载完成时间<br> 对于移动端或者pc端,有很多应用是js控制出现一个loading动画,当有数据了在去掉loading, js控制出现loading的时刻就是页面加载完成时间<br> 如果页面很长,开始没有滚动条,当滚动条出现的时候,就是页面加载完成时间<br> </pre> 资源下载完成时间:可以看到页面上的图片、看到iframe 嵌套页面里面的内容,浏览器标签不在出现loding不断旋转的时刻,就是资源加载完成的时间点 <img src="/images/session/bi2.png" alt="ruby-prof"> <br> 整页时间:页面彻底加载完成的时间,一般情况下资源下载完成时间和整页面时间很接近<br></p> <h2>影响 页面加载时段 的主要因素</h2> <p>1: 基础的网络质量<br> 2: 网页html文件的大小 例如 你是10k,1m 10m<br> 3:是页面中通过scrtpt标签引入的脚本下载和执行,包括head中的也包括body中<br> <pre> 因为一个浏览器同时可以可以下载的文件是有限的,同一个域名下可以同时下载的文件也是有限的<br> 总的来说一般的浏览器为2~4 性能好点的浏览器是8个左右<br> 所以这个阶段网页中下载 css,图片等资源,都会影响页面加载时段的面积<br> 因为css影响页面样式,所以一般放在head里面,公共的js文件放在head里面,业务的js建议通过动态加载 </pre></p> <h2>影响 资源下载时段 的主要因素</h2> <p>1:基础的网络质量<br> 2:页面中图片、视频、iframe 嵌套页,广告等,都在这个阶段下载<br> 3:下载资源个数和域名数,因为浏览器从同一个域名下同时下载资源是有限的,所以域名个数个和下载文件的个数都会影响整体性能<br> 所以一般都会把一个域名下载资源分散到3个左右域名下,同时下载,提高页面性能<br> 4:第三方资源 例如百度统计、谷歌统计、 百度地图 一般都会在这个阶段加载,如果第三方资源处现在问题,资源下载时段面积也会增大</p> <h2>查看那些资源的下载拖慢了整个页面的速度</h2> <p>OneAPM Browser Insight 的 trace 会收集缓慢页面的资源加载信息,可以作为性能优化的参考 <img src="/images/session/bi5.png" alt="ruby-prof"></p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 被遗忘的Android mipmaps简介 /android/2015/08/25/android3/ Tue, 25 Aug 2015 00:00:00 +0000 /android/2015/08/25/android3 <h1>被遗忘的Android mipmaps简介</h1> <p>【<strong>导读</strong>】已经发布的Android Studio1.1版本是一个bug修复版本。在这个版本中,当你创建工程时一项改变将会吸引你的眼球。工程创建登陆的图标会在mipmap-resource文件夹中,而不是drawable文件夹中。</p> <h1>使用mipmap文件夹存放启动图标的好处</h1> <p>Android开发团队认为使用mipmap文件夹保存启动图标是最佳的开发方式。使用mipmap的最大优势是你可以在各种屏幕密度的设备下将资源存储在mipmap文件夹下,然后对于特定屏幕密度的设备你可以从drawable文件夹下去除掉与这个特定设备无关的资源信息。</p> <p>我们用一个例子来说明这一特性。假设用户有一个设备被标识为xxhdpi的设备,那么开发者apk中的drawable文件夹下的其他分辨率大小的资源,例如xxxhdpi分辨率,就是不被需要的;换句话说,就是可以被去除掉的。然而应用图标很可能是与其他资源图片不同的,因为他可能占据整个屏幕,因此你可能需要启动图标的分辨率高于其他资源。而这时你为了设备适配的原因,高分辨率图片所在的文件夹已经被你去除了,这时当程序运行时,系统会将原来低分辨率的图片进行拉伸,然而这种做法可能会导致产生一个丑陋的模糊的图片。</p> <p>另一方面,你可能会说,你并不会使用去除资源来减少apk大小的方式去制作不同版本的apk文件。这个时候你用mipmap文件夹去存储登陆图标看上去不会产生任何意义。然而这并不意味着你不应该使用它,最好的使用它的理由莫过于Android开发团队认为应该使用它,并且把它描述为了开发的最佳实践。他们知道在Android的开发线上将会产生什么样的新特性,而且尽管没有人能确定,但是Android开发团队很可能计划引入这样一个新特性,就是在用户下载App的时候,apk会自动丢弃掉不合适的资源文件</p> <h1>Android 登陆mipmap的历史</h1> <p>在Android中mipmap资源文件夹很早就出现了,这个时间点远远早于他被介绍为应该作为Android开发社区应该大规模使用它的时间。Android的官方团队并没有发布过多的原因来说明为什么要使用mipmap资源文件夹。在Android 4.3版本发布的时候,Android 框架的卡法人员hackbod宣布: 假如你正在为不同屏幕分辨率版本的设备制作不同版本的app,那么你应该去了解mipmap资源文件夹,它与drawable文件夹确实是惊人的相似,除了它并不参与屏幕适配在你生成不同版本的目标apk时。 自从这个宣布之后,如果你仔细观察Google的产品,你会发现一些app确实这样做了。效果是在展示时,仅仅只是向屏幕传输了所被需要的分辨率的图标,但是各种分辨率的登陆图标都别保存到了mipmap资源文件夹下。 随着时间的推进,并没有过多关于使用mipmap资源文件夹保存登陆图片的消息,知道Android 5.0出现在AOSP以及Nexus设备出现在了市场上。Android开发者的博客上标出了准备好让你的app登陆Nexus6 和Nexus9. 这篇博文透露了一个应该使用mipmap文件夹的理由: 提供分辨率至少为xxxhpdi的app图标因为身背可以展示更大的图标在登陆的时刻。最好的方法就是见app图标放在mipmap文件夹下因为他将会根据屏幕分辨率以及使用场景选择合适分辨率的图片。例如 xxxhdpi的图标将会被展示在xxhdpi分辨率的设备上</p> <h1>使用mipmap创建Android工程</h1> <p>更新Android Studio到最新版本,任何一个新的工程都将会使用mipmap的方式创建。</p> <h1>将现有的工程改变为使用mipmap文件夹</h1> <p>假如你有一个优秀的工具Android Asset Studio去产生你的Android登陆图标,你会注意到mipmap的改变已经被assets文件夹产生的结构所反映出来。 你的assets应该变得像之前的登陆图标的结构那样。 res/ mipmap-mdpi/ic<em>launcher.png (48x48 pixels) mipmap-hdpi/ic</em>launcher.png (72x72) mipmap-xhdpi/ic<em>launcher.png (96x96) mipmap-xxhdpi/ic</em>launcher.png (144x144) mipmap-xxxhdpi/ic<em>launcher.png (192x192) 同时你要改变你的AndroidManifest.xml文件去使用mipmap作为启动图标: android:icon=&quot;@mipmap/ic</em>launcher&quot;</p> <p><strong>本文系<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创。想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> JavaScript 堆内存分析新工具 OneHeap /javascript/2015/08/03/oneheap/ Mon, 03 Aug 2015 00:00:00 +0000 /javascript/2015/08/03/oneheap <blockquote> <p>OneHeap 关注于运行中的 JavaScript 内存信息的展示,用可视化的方式还原了 HeapGraph,有助于理解 v8 内存管理。</p> </blockquote> <h2>背景</h2> <p>JavaScript 运行过程中的大部分数据都保存在堆 (Heap) 中,所以 JavaScript 性能分析另一个比较重要的方面是内存,也就是堆的分析。</p> <p>利用 Chrome Dev Tools 可以生成应用程序某个时刻的堆快照 (HeapSnapshot),它较完整地记录了各种对象和引用的情况,堪称查找内存泄露问题的神器。 和 Profile 结果一样,快照可以被导出成 <code>.heapsnapshot</code> 文件。</p> <p><img src="/images/oneheap/dog.png" alt="heapsnapshot"></p> <p>上周发布了工具 <a href="http://wyvernnot.github.io/javascript_performance_measurement/cpuprofile_topology/">OneProfile</a> , 可以用来动态地展示 Profile 的结果,分析各种函数的调用关系。周末我用类似的思路研究了一下 <code>.heapsnapshot</code> 文件,做了这个网页小工具,把 Heap Snapshot 用有向图的方式展现出来。</p> <p><img src="/images/oneheap/screenshot.gif" alt="screenshot"></p> <h2>OneHeap 名字的由来</h2> <blockquote> <p>There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton</p> </blockquote> <p>目前还没有时间想一个高端、大气、上档次的名字,因为我供职的公司名叫 <a href="http://www.oneapm.com/info/about.html">OneAPM</a> ( 省去软广1000字,总之做性能监控很牛),所以就取名 OneHeap 啦。 它是 <a href="https://github.com/wyvernnot/javascript_performance_measurement/">Toolkit</a> 里的第二个。</p> <h2>如何生成 Heap Snapshot 文件</h2> <p><strong>浏览器</strong></p> <p>使用 Chrome 打开 <a href="http://wyvernnot.github.io/javascript_performance_measurement/heap_snapshot/dog.html">测试页面</a> 按 F12 打开 <code>Devtools</code>,切换到 <code>Profiles</code> 页,选择 <code>Take Heap Snapshot</code>。稍等片刻,在生成的 <code>Snapshot</code> 上点击右键可以导出,文件后缀一般是 <code>.heapsnapshot</code>。</p> <p><strong>Node.JS</strong></p> <p>如果你是 Node.JS 工程师,可以安装 <code>heapdump</code> 这个很有名的模块。</p> <p><a href="https://github.com/bnoordhuis/node-heapdump">https://github.com/bnoordhuis/node-heapdump</a></p> <p>上面两种方法都可以生成 <code>.heapsnapshot</code> 文件,这个是用来测试的 <a href="http://wyvernnot.github.io/javascript_performance_measurement/heap_snapshot/nodejs.heapsnapshot">nodejs.heapsnapshot</a></p> <h2>理解 .heapsnapshot 文件格式</h2> <p>打开测试用的 <code>nodejs.heapsnapshot</code> 文件,这是一个很大的 JSON 对象:</p> <p>1. <code>snapshot</code> 属性保存了关于快照的一些基本信息,如 uid,快照名,节点个数等</p> <p>2. <code>nodes</code> 保存了是所有节点的 id,name,大小信息等,对应 v8 源码里的 <a href="https://github.com/joyent/node/blob/master/deps/v8/include/v8-profiler.h#L219"><code>HeapGraphNode</code></a></p> <p>3. <code>edges</code> 属性保存了节点间的映射关系,对应 v8 源码的 <a href="https://github.com/joyent/node/blob/master/deps/v8/include/v8-profiler.h#L183"><code>HeapGraphEdge</code></a></p> <p>4. <code>strings</code> 保存了所有的字符串, <code>nodes</code> 和 <code>edges</code> 中不会直接存字符串,而是存了字符串在 <code>strings</code> 中的索引</p> <p>堆快照其实是一个有向图的数据结构,但是 <code>.heapsnapshot</code> 文件在存储的过程中使用了数组来存储图的结构,这一设计十分巧妙而且减少了所需磁盘空间的大小。</p> <h3>nodes 属性</h3> <p>nodes 是一个很长一维的数组,但是为了阅读方便,v8 在序列化的时候会自动加上换行。按照 v8 版本的不同,可能是5个一行,也可能是6个一行,如果是 6 个一行,则多出来的一个 <code>trace_node_id</code> 属性。</p> <table><thead> <tr> <th>下标</th> <th>属性</th> <th>类型</th> </tr> </thead><tbody> <tr> <td>n</td> <td>type</td> <td>number</td> </tr> <tr> <td>n+1</td> <td>name</td> <td>string</td> </tr> <tr> <td>n+2</td> <td>id</td> <td>number</td> </tr> <tr> <td>n+3</td> <td>self_size</td> <td>number</td> </tr> <tr> <td>n+4</td> <td>edge_count</td> <td>number</td> </tr> </tbody></table> <p>其中 type 是一个 0~12 的数字,目前的 Chrome 只有 0~9 这几个属性,它们对应的含义分别是</p> <table><thead> <tr> <th>编号</th> <th>属性</th> <th>说明</th> </tr> </thead><tbody> <tr> <td>0</td> <td>hidden</td> <td>Hidden node, may be filtered when shown to user.</td> </tr> <tr> <td>1</td> <td>array</td> <td>An array of elements.</td> </tr> <tr> <td>2</td> <td>string</td> <td>A string.</td> </tr> <tr> <td>3</td> <td>object</td> <td>A JS object (except for arrays and strings).</td> </tr> <tr> <td>4</td> <td>code</td> <td>Compiled code.</td> </tr> <tr> <td>5</td> <td>closure</td> <td>Function closure.</td> </tr> <tr> <td>6</td> <td>regexp</td> <td>RegExp.</td> </tr> <tr> <td>7</td> <td>number</td> <td>Number stored in the heap.</td> </tr> <tr> <td>8</td> <td>native</td> <td>Native object (not from V8 heap).</td> </tr> <tr> <td>9</td> <td>synthetic</td> <td>Synthetic object, usualy used for grouping snapshot items together.</td> </tr> <tr> <td>10</td> <td>concatenated string</td> <td>Concatenated string. A pair of pointers to strings.</td> </tr> <tr> <td>11</td> <td>sliced string</td> <td>Sliced string. A fragment of another string.</td> </tr> <tr> <td>12</td> <td>symbol</td> <td>A Symbol (ES6).</td> </tr> </tbody></table> <h3>edges 属性</h3> <p><code>edges</code> 也是一个一维数组,长度要比 <code>nodes</code> 大好几倍,并且相对于 <code>nodes</code> 要复杂一些:</p> <table><thead> <tr> <th>下标</th> <th>属性</th> <th>类型</th> </tr> </thead><tbody> <tr> <td>n</td> <td>type</td> <td>number</td> </tr> <tr> <td>n+1</td> <td>name<em>or</em>index</td> <td>string<em>or</em>number</td> </tr> <tr> <td>n+2</td> <td>to_node</td> <td>node</td> </tr> </tbody></table> <p>其中 type 是一个 0~6 的数字:</p> <table><thead> <tr> <th>编号</th> <th>属性</th> <th>说明</th> </tr> </thead><tbody> <tr> <td>0</td> <td>context</td> <td>A variable from a function context.</td> </tr> <tr> <td>1</td> <td>element</td> <td>An element of an array</td> </tr> <tr> <td>2</td> <td>property</td> <td>A named object property.</td> </tr> <tr> <td>3</td> <td>internal</td> <td>A link that can&#39;t be accessed from JS,thus, its name isn&#39;t a real property name (e.g. parts of a ConsString).</td> </tr> <tr> <td>4</td> <td>hidden</td> <td>A link that is needed for proper sizes calculation, but may be hidden from user.</td> </tr> <tr> <td>5</td> <td>shortcut</td> <td>A link that must not be followed during sizes calculation.</td> </tr> <tr> <td>6</td> <td>weak</td> <td>A weak reference (ignored by the GC).</td> </tr> </tbody></table> <p><strong>nodes 和 edges 的对应关系</strong></p> <p>如果知道某个节点的 id,是没有办法直接从 <code>edges</code> 中查出和它相邻的点的,因为 <code>edges</code> 并不是一个 <code>from-to</code> 的 Hash。想知道从一个节点出发 可到达那些节点,需要遍历一次 <code>nodes</code>。</p> <p>具体做法如下:</p> <p>1. 在遍历 <code>nodes</code> 前初始化一个变量 <code>edge_offset</code>,初始值是0,每遍历一个节点都会改变它的值。</p> <p>2. 遍历某个节点 Nx 的过程中:</p> <p>从 Nx 出发的第一条 Edge</p> <div class="highlight"><pre><code class="language-text" data-lang="text">edges[ edge_offset ] 是 Edge 的类型 edges[ edge_offset +1 ] 是 Edge 的名称或下标 edges[ edge_offset +2 ] 是 Edge 指向的对象的节点类型在 `nodes` 里的索引 </code></pre></div> <p>从 Nx 出发的第2条 Edge</p> <div class="highlight"><pre><code class="language-text" data-lang="text">edges[ edge_offset + 3 ] ............ 是下一个 Edge edges[ edge_offset + 5 ] </code></pre></div> <p>从 Nx 出发,一共有 <code>edge_count</code> 条 Edge </p> <p>...</p> <p>3. 每遍历完一个节点,就在 <code>edge_offset</code> 上加 <code>3 x edge_count</code>,并回到步骤 2,直到所有节点都遍历完</p> <p>步骤1到3 用伪代码表示就是:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">edge_offset=0 // 遍历每一个节点 for(node in nodes){ // edges 下标从 edge_offset 到 edge_offset + 3 x edge_count 都是 node 可以到达的点 edge_offset+= 3 x node.edge_count } </code></pre></div> <p>以上就是 <code>.heapsnapshot</code> 的文件格式定义了,基于这些发现,在结合一个前端绘图的库,就可以可视化的展示 Heap Snapshot 了。</p> <h2>OneHeap 使用说明</h2> <p><strong>链接地址</strong></p> <p>使用 Chrome 打开: <a href="http://wyvernnot.github.io/javascript_performance_measurement/heap_snapshot/">OneHeap</a></p> <h3>一些有意思的截图</h3> <p><strong>@1</strong></p> <p>Node.JS</p> <p><img src="/images/oneheap/root.png" alt="root"></p> <p><em>朴灵老师的《深入浅出Node.JS》有对 Buffer 的详细介绍,其中提到 Buffer 是 JavaScript 和 C++ 技术结合的典型代表</em></p> <p>浏览器</p> <p><img src="/images/oneheap/dom.png" alt="dom"></p> <p><em>很明显浏览器下多了 Window 和 Document 对象,而 Detached DOM tree 正是前端内存泄露的高发地。</em></p> <p><strong>Objects</strong></p> <p><img src="/images/oneheap/objects.png" alt="root"></p> <p><em>最密集的那部分的中心是 Object 构造函数,如果把 Object 和 Array 构造函数隐藏,就变成了下面这样</em></p> <p><img src="/images/oneheap/no_objects.png" alt="root"></p> <p><strong>MathConstructor</strong></p> <p><img src="/images/oneheap/math.png" alt="Math"></p> <p>左上角是例如 <code>自然对数E</code> 这样的常量,<a href="https://github.com/joyent/node/blob/master/deps/v8/src/math.js#L380">v8源码</a></p> <p><strong>正则表达式</strong></p> <p><img src="/images/oneheap/regexp.png" alt="Regexp"></p> <p>所有的正则表达式实例的 <code>__proto__</code> 都指向 RegExp 构造函数,同时 RegExp 的 <code>__proto__</code> 又指向 Object</p> <p><strong>Stream</strong></p> <p><img src="/images/oneheap/stream_inherit.png" alt="Stream"></p> <p>在 Node.JS 中和 Stream 相关的几个类的设计和 <code>Java</code> 类似,都使用到装饰器的设计模式,层层嵌套, 例如 <a href="https://github.com/joyent/node/blob/master/lib/_stream_writable.js#L159">v8源码</a></p> <h3>参考资料</h3> <p><a href="https://developers.google.com/chrome-developer-tools/docs/heap-profiling">Heap Profiling</a></p> <p><a href="http://www.ibm.com/developerworks/cn/web/wa-jsmemory/">了解 JavaScript 应用程序中的内存泄漏</a></p> <h3>关于</h3> <p>本文相关的源码在: <a href="https://github.com/wyvernnot/javascript_performance_measurement/tree/gh-pages/heap_snapshot">https://github.com/wyvernnot/javascript<em>performance</em>measurement/tree/gh-pages/heap_snapshot</a>;</p> <blockquote> <p>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创 ,想阅读更多技术文章,请访问<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM官方技术博客</a>。</p> </blockquote> JavaScript 性能分析新工具 OneProfile /javascript/2015/07/28/oneprofile/ Tue, 28 Jul 2015 00:00:00 +0000 /javascript/2015/07/28/oneprofile <blockquote> <p>OneProfile 是一个网页版的小工具,可以用全新的方式展示 JavaScript 性能分析的结果,帮助开发者洞悉函数调用关系,优化应用性能。</p> </blockquote> <p><a href="http://wyvernnot.github.io/javascript_performance_measurement/cpuprofile_topology/">点击打开 OneProfile</a></p> <h2>背景</h2> <p>Chrome Dev Tools 自带的 CPU Profile 功能非常好用。用它可以方便的生成 JavaScript 的 <code>Flame Chart</code>。</p> <p><img src="/images/oneprofile/screenshot_chrome.png" alt="Flame Chart"></p> <p>更棒的是你可以把 <code>Flame Chart</code> 导出,留着下次或者拷贝到其它机器上查看,特别好奇它是怎么实现的。</p> <p>但是网上关于它的文件格式以及怎么画图的文档非常稀有,所以我自己摸索了一下它的文件格式,并尝试着用另一种方式展示 CPU Profile 的结果。</p> <h2>如何生成 CPU Profile 文件</h2> <p>使用最新版的 Chrome 打开任意一个 <a href="http://oneapm.com">测试网站</a>,按 F12 打开 <code>Devtools</code>, 切换到 <code>Profiles</code> 页,点击 <code>Start</code> 开始 收集 Profile 信息,在当前页面任意滑动鼠标等待大约5秒后, 点击 <code>Stop</code> 停止 Profile。在生成的 CPU Profile 名字上单击右键可以导出 <code>.cpuprofile</code> 后缀名的文件。</p> <p>你可以自己生成一个,也可以直接下载这个用来测试 <a href="https://raw.githubusercontent.com/wyvernnot/javascript_performance_measurement/gh-pages/cpuprofile_topology/sample.cpuprofile">sample.cpuprofile</a></p> <h2>理解 .cpuprofile 文件格式</h2> <p>用你的编辑器打开 <code>sample.cpuprofile</code> ,你会惊奇的发现:</p> <p>1. <code>sample.cpuprofile</code> 其实就是一个 JSON 格式的文件,有 <code>head</code>, <code>timestamps</code>, <code>samples</code> 等几个重要的属性</p> <p>2. <code>head</code> 指向一个结点,官方的名字叫<code>CpuProfileNode</code>,同时它的 <code>children</code> 指向子结点, 因此是一个嵌套结构</p> <p>3. <code>CpuProfileNode</code> 有很多重要的属性,包括 functionName,lineNumber,columnNumber,hitCount 等</p> <p>4. <code>timestamps</code> 是一个数组,记录着 Profiling 过程中每个采样点的时间戳</p> <p>5. 对应 <code>timestamps</code> 下的每个时间点,<code>samples</code> 数组相同的位置都会有一个数字,这个数字比较神秘,后面解释</p> <p><strong>CpuProfileNode 详解</strong></p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="p">{</span> <span class="s2">&quot;functionName&quot;</span><span class="o">:</span> <span class="s2">&quot;lineTo&quot;</span><span class="p">,</span> <span class="s2">&quot;scriptId&quot;</span><span class="o">:</span> <span class="s2">&quot;0&quot;</span><span class="p">,</span> <span class="s2">&quot;url&quot;</span><span class="o">:</span> <span class="s2">&quot;&quot;</span><span class="p">,</span> <span class="s2">&quot;lineNumber&quot;</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">&quot;columnNumber&quot;</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">&quot;hitCount&quot;</span><span class="o">:</span> <span class="mi">45</span><span class="p">,</span> <span class="c1">// 被采样到的次数</span> <span class="s2">&quot;callUID&quot;</span><span class="o">:</span> <span class="mi">6</span><span class="p">,</span> <span class="c1">// 函数入口的 UID</span> <span class="s2">&quot;children&quot;</span><span class="o">:</span> <span class="p">[],</span> <span class="s2">&quot;positionTicks&quot;</span><span class="o">:</span> <span class="p">[</span> <span class="p">{</span> <span class="s2">&quot;line&quot;</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&quot;ticks&quot;</span><span class="o">:</span> <span class="mi">45</span> <span class="p">}</span> <span class="p">],</span> <span class="s2">&quot;deoptReason&quot;</span><span class="o">:</span> <span class="s2">&quot;&quot;</span><span class="p">,</span> <span class="c1">// 逆优化的原因</span> <span class="s2">&quot;id&quot;</span><span class="o">:</span> <span class="mi">11</span> <span class="p">}</span> </code></pre></div> <p><strong>samples, timestamps 和 CpuProfileNode 的关系</strong></p> <p>如果从 <code>head</code> 开始,对 <code>head</code> 结点及其 <code>children</code> 属性下的结点做一次深度优先的遍历,每个可能路径都会有一个编号。 研究表明这个数字正对应于 <code>samples</code> 的值。因此知道了路径编号,便可以知道那些函数处在激活状态。在 <code>OneProfile</code> 中用蓝色表示。</p> <h2>OneProfile 使用说明</h2> <p><strong>链接地址</strong></p> <p>使用 Chrome 打开: <a href="http://wyvernnot.github.io/javascript_performance_measurement/cpuprofile_topology/">OneProfile</a></p> <p><strong>图例</strong></p> <p><code>黑色</code> 系统函数</p> <p><code>暗红色</code> 存在逆优化的函数,鼠标悬停可见原因</p> <p><code>蓝色</code> 当前采样点活动的函数</p> <p><strong>操作</strong></p> <p><code>滚轮</code> 缩放窗口</p> <p><code>prev</code> 前一个采样点</p> <p><code>next</code> 后一个采样点</p> <p><strong>一些用例</strong></p> <p>某在线地图网站的前端代码,红色较多说明存在大量 v8 无法优化的代码</p> <p><img src="/images/oneprofile/baidumaps.png" alt="baidu"></p> <p>是不是很像外星人</p> <p><img src="/images/oneprofile/xiaodian.jpg" alt="xiaodian"></p> <p>( 请原谅作者笑点低 &gt;&lt; )</p> <h3>关于</h3> <p>本文相关的源码在: <a href="https://github.com/wyvernnot/javascript_performance_measurement/tree/gh-pages/cpuprofile_topology">https://github.com/wyvernnot/javascript<em>performance</em>measurement/tree/gh-pages/cpuprofile_topology</a>;</p> <blockquote> <p>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创 ,想阅读更多技术文章,请访问<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM官方技术博客</a>。</p> </blockquote> 移动开发者们,是时候使用HTML5了! /android/2015/07/09/android2/ Thu, 09 Jul 2015 00:00:00 +0000 /android/2015/07/09/android2 <h1>移动开发者们,是时候使用HTML5了!</h1> <p>【<strong>导读</strong>】由于我一直做移动APP的开发,多终端同一套逻辑开发多次不说,同时开发人员还要和UI和产品多次沟通来实现,身为App开发者,这里面的苦,我有发言权。所以,特别希望看见一款跨平台的Framework能Cover住所有平台,真正能达到一次编写,所有移动终端运行的目的 我想你和我也一样,希望有这么一个产品。今天,它来了我们认识下吧。</p> <p>Html5喊了好多年了,至今仍没有被大规模的使用。依然记得2012年参加HTML5梦工厂(现在叫iWeb峰会)去了好多人,当时天真的以为,Html5真的开始流行起来了,于是就在会场卖书的地方买了本Html5的书来学。后来,大家知道的,Html5都是不温不火,但是我却没有减少对其关注的热情。 由于我一直做移动APP的开发,多终端同一套逻辑开发多次不说,同时开发人员还要和UI和产品多次沟通来实现,身为App开发者,这里面的苦,我有发言权。所以,特别希望看见一款跨平台的Framework能Cover住所有平台,真正能达到一次编写,所有移动终端运行的目的。中间接触了Hbuilder(这个是在14年iWeb峰会上看见的)、AppCan、WeX5、ApiCloud等。仅限于了解没有太深入,大致的结果如下表(对比结果都是本人实践的观点): <table class="confluenceTable tablesorter"><thead><tr class="sortableHeader"><th class="confluenceTh sortableHeader tablesorter-headerSortDown" data-column="0"><div class="tablesorter-header-inner">框架名称</div></th><th class="confluenceTh sortableHeader" data-column="1"><div class="tablesorter-header-inner">优点</div></th><th class="confluenceTh sortableHeader" data-column="2"><div class="tablesorter-header-inner">缺点</div></th></tr></thead><tbody class=""><tr><td colspan="1" class="confluenceTd">ApiCloud</td><td colspan="1" class="confluenceTd"><p>号称重新定义移动开发有自己的BAAS服务,直接后台编辑数据库表,直接生</p><p>产了对应REST API,爽前所未有。有自己的Store,里面有一些第三方组件,</p><p>可以使用。</p></td><td colspan="1" class="confluenceTd"><p>产品上为了和Baas结合有端API,服务端Api,得折</p><p>腾下才能跑起来,要学的东西稍多。对于没有Server</p><p>开发经验的人来说可以只看端Api。</p></td></tr><tr><td class="confluenceTd">AppCan</td><td class="confluenceTd"><p>应该是自己研发的移动App跨平台开发引擎,应该对自己比较有信心,目前已</p><p>经开源,赞!对于喜欢折腾的人来说是一个福音。有商业案例,我下载了一个</p><p>吉祥航空如E行的案例运行了下,基本能满足需求。没仔细往下研究了。</p></td><td class="confluenceTd"><p>从吉祥航空如E行 (2.0.3)这个APP来看,体验上还</p><p>有待加强,值得开发者试一下。</p></td></tr><tr><td class="confluenceTd">Hbuilder</td><td class="confluenceTd"><p>使用html5plus来弥补原生Webview性能不足的问题;并且很好解决了如何</p><p>和Native通信的问题,在Js中可以实例化Native的组件等等。Demo运行起来</p><p>也比较流畅。是这四个里面体验最好的吧!有自己的Hbuilder编辑器好用。</p></td><td class="confluenceTd"><p>文档、教程不是很完善。自己虽然有一个UI框架</p><p>但是对我这样的菜鸟来说上手还是一个问题。最终</p><p>做了一个简单的APP之后,接触的比较少了。</p></td></tr><tr><td class="confluenceTd">WeX5</td><td class="confluenceTd"><p>号称真正的全平台,微信、安卓、IOS 网站,真正的一次开发到处运行啊。由</p><p>于文档教程不是很顺畅,没有深入研究。</p></td><td class="confluenceTd">文档不是很多,上手需要花点时间。</td></tr></tbody></table> 从上面的总结来看,Html5虽然没有来,对各个厂商来说都在磨拳擦掌,场面可以说是已经战火四起了。不过,对于开发者的我们来说是一个利好,我喜欢。不过今日我要介绍的不是上面的框架,而是国外一个比较流行的框架叫Ionic,其实要说跨平台开发上面的几个框架都能满足,不过性能和体验上的问题都是User Sensitive的,不得不考虑。这也是我推荐Ionic的原因,它专注体验极度优化,贴近原生。按照官方的教程和资源Step By Step 很快我就搭建了一个TODO的App~~~体验地址(安卓版)<a href="https://github.com/it114/ionic-todo-project/blob/master/oneapm-ionic-app-todo.apk">传送门-点击下载</a> APP截图: <img src="https://wt-prj.oss.aliyuncs.com/4f36e28f07b64baf92be0a7d519a8689/465e2ca8-9333-45f5-983f-56feec39c292.jpg" alt="主页"> <img src="https://wt-prj.oss.aliyuncs.com/4f36e28f07b64baf92be0a7d519a8689/1362e9ac-e6ca-4b29-880e-557a1201dede.jpg" alt="侧边栏"></p> <hr> <p>好了,到此为止,下载体验过这个APP的如果觉得 体验上不够好可以不用往下看了。因为往下面说的都是关于Ionic的知识喽;如果您觉得体验很赞,想研究的,我把源码放在了github:<a href="https://github.com/it114/ionic-todo-project">前往ionic-todo APP 源码</a>。有需要的自己可以下载编译,研究。</p> <p>不好意思,说了那么久才进入正题,这也算是史上最长的开场白了吧!</p> <h2>什么是Ionic,它适合做什么?</h2> <p>ionic是一个专注于开发与构建Hybird模式跨平台的HMTL5框架。Hybird模式的APP相当于拥有访问本地设备接口(例如访问相机、文件系统,传感器)能力的并且运行在一个浏览器外壳的一个微型网站。因此Hybird模式开发的APP有许多原生APP所没有的优势;特别是在跨平台支持、快速开发以及快速访问第三方代码方面。 ionic可以想象为处理让APP拥有令人瞩目的UI交互体验以及漂亮的外观的一个前端框架。有点类似于Bootstrap for native,不过ionic支持更广泛和更贴近原生native mobile的组件。 和响应式框架不同的是,ionic与生俱来拥有已经存在的网页开发中所不存在的非常贴近于native mobile的样式和UI 组件、布局。并且还提供了一些可选的并且强大的方法去构建基于已经存在的Html5开发框架。 既然ionic是一个专注于体验和运行效率的HTML5开发框架,它需要一些类似于Cordova或者PhoneGap的一些包装器去作为一个原生APP运行。强烈推荐使用Cordova,ionic的一些工具都是基于Cordova的。</p> <h2>Ionic的由来</h2> <p>开发ionic的团队说,他们强烈的意识到HTML5将会一直运行在移动设备上,就像HTML5已经运行在PC电脑上一样。一旦计算机变的足够强大并且浏览器技术变的足够的先进,几乎所有的人都将被迫花费时间去和浏览器打交道。开发者已经开发了大量的Web站点和应用程序,近来由于移动领域开发技术的进步,手机、平板电脑目前也能运行这些Web应用程序了。 使用Ionic Html5框架,能够去构建基于nativie或者hybird app而非是用来开发移动站点的,因为目前市场上已经存在了大量的用来开发移动站点的开发工具。所以,Ionic app不是被设计用来运行在Chrome或者Safari,而是被设计运行在更低层次的被Cordova或者PhoneGap包装过的浏览器内核中,例如IOS的UIWebView或者安卓的WebView。</p> <h2>用Ionic开发Hybrid App</h2> <p>熟悉Web的开发者将会发现Ionic的程序结构和Web开发比较类似。从核心上说,一个Ionic App只不过是一个网页运行在一个Native App的壳里面。也就意味着,你可以用任何你熟悉的HTML、CSS、Javascript技术来是开发Ionic App。不同于网页通过url链接来跳转,使用Ionic是开发一个自容器(译者注:可以理解为单页应用程序,跳转是基于锚点的)的App的体验。 开发基于Ionic框架的APP需要使用HTML、CSS、Javascript,一些着急的开发者可能要深入到native层面去研究Corvoda插件或者Native代码,这对于开发一个伟大App是没有必要的(译者注:也可以去折腾)。</p> <h2>ionic性能</h2> <p>ionic性能比较好,体验很赞,这也是我写这篇文章的去推荐使用的目的。具体如何,看官们可以使用OneAPM的性能监控工具来(监控WebView功能)测试下,下面是一个监控百度首页加载的页面截图, <img src="https://wt-prj.oss.aliyuncs.com/4f36e28f07b64baf92be0a7d519a8689/7e2cce65-71e0-417e-a52d-94286007a4ef.png" alt="OneAPM监控baidu.com"></p> <p>Ionic是构建Hybird App的一个相对较新的框架,版本刚从bate版本到1.0版本,其中1.0版本的修正版有好多个才推出正式版,可见已经足够稳定了。并且在github上被标星过17766次,fork过2386次了,足见其受欢迎程度。是时候拥抱H5了!</p> <p>ionic介绍部分参考:The Ionic Book<a href="http://ionicframework.com/docs/guide/preface.html">第一章</a></p> <p><strong>本文系<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创。想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Webpack 性能优化 (一) /javascript/2015/07/07/webpack_performance_1/ Tue, 07 Jul 2015 00:00:00 +0000 /javascript/2015/07/07/webpack_performance_1 <h2>前言</h2> <p>Webpack 是 OneAPM 前端技术栈中很重要的一部分,它非常好用,如果你还不了解它,建议你阅读这篇 <a href="http://segmentfault.com/a/1190000002551952">Webpack 入门指迷</a> ,在 OneAPM 我们用它完成静态资源打包,ES6 代码的转换 ,React 组件的组织等,在接下来的日子里,我们将通过一系列文章和业界分享我们在使用 Webpack 过程中关于性能方面的经验。</p> <p>作为系列文章的第一篇,我们会重点介绍 Webpack 中的 <code>resolve.alias</code> ,也就是请求重定向。不过请注意 Webpack 里的请求是对模块的依赖,也就是一个 <code>require</code> 语句,而不是一个 HTTP 请求。</p> <p><strong>必要的准备</strong></p> <ul> <li>需要你有一定的 Node.js 基础</li> <li>电脑上装有最新版的 Webpack (<code>npm install webpack -g</code>)</li> <li>了解 Webpack 配置文件的格式</li> </ul> <p><strong>例子:本地时钟</strong></p> <p>要实现的功能很简单,就是在页面上用中文显示当前时间,需要用到 <a href="http://momentjs.com/"><code>moment</code></a> 这个库,这个库封装了很多和日期相关的函数,而且自带了国际化的支持。</p> <h3>新建一个 Node.js 项目</h3> <p>使用 <code>npm init</code> 初始化你的项目,然后通过 <code>npm install moment -D</code> 加上 <code>moment</code> 的开发者依赖。</p> <p>新建一个 <code>entry.js</code> 作为入口文件,当然你也可以用 <code>app.js</code> 这样的名字,只是大部分的 Webpack 示例都是用的是 <code>entry.js</code>。</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">moment</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;moment&#39;</span><span class="p">);</span> <span class="nb">document</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">moment</span><span class="p">().</span><span class="nx">locale</span><span class="p">(</span><span class="s1">&#39;zh-cn&#39;</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;LLLL&#39;</span><span class="p">));</span> </code></pre></div> <p>新建一个页面 <code>index.html</code>, 引用 <code>bundle.js</code>:</p> <div class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;body&gt;</span> <span class="nt">&lt;h5&gt;</span>当前时间:<span class="nt">&lt;/h5&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">&quot;dist/bundle.js&quot;</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;/body&gt;</span> </code></pre></div> <p>此时的文件目录看起来是这样的:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">index.html package.json entry.js node_modules/moment </code></pre></div> <p>到目前为止 <code>bundle.js</code> 这个文件还不存在,不过别着急,接下来的工作就交给 Webpack 来完成。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">index.html ------------------------+ package.json | +--&gt; &lt;Clock App&gt; entry.js --------+ | +--&gt;bundle.js+--+ node_modules/moment-+ </code></pre></div> <p>如图,Webpack 会把 <code>entry.js</code> 和 <code>moment</code> 模块一起打包成一个 bundle.js 文件,和 <code>index.html</code> 一起构成了我们的 Clock App。怎么样,是不是已经听到 Clock App 滴答作响了?</p> <h3>使用 webpack 打包代码</h3> <p>在命令行执行:</p> <div class="highlight"><pre><code class="language-sh" data-lang="sh">webpack --entry ./entry.js --output-path dist --output-file bundle.js </code></pre></div> <p>你会看到类似下面的输出结果:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Hash: bf9007fb1e0cb30e3ef7 Version: webpack 1.10.0 Time: 650ms Asset Size Chunks Chunk Names bundle.js 378 kB 0 [emitted] null [0] ./entry.js 125 bytes {0} [built] + 86 hidden modules </code></pre></div> <p>可以看到,耗时 650ms,这么慢着实让人意外,一定要想办法提高“新一代神器”速度;另一方面,最后一行的 <strong>+ 86 hidden modules</strong> 非常让人怀疑:明明是一个简单的 Clock App,怎么会有这么多的依赖。</p> <h2>如何快速定位 Webpack 速度慢的原因</h2> <p>再一次,在命令行输入:</p> <div class="highlight"><pre><code class="language-sh" data-lang="sh">webpack --entry ./entry.js --output-path dist --output-file bundle.js <span class="se">\</span> --colors <span class="se">\</span> --profile <span class="se">\</span> --display-modules </code></pre></div> <p>不过这次新增加了三个参数,这三个参数的含义分别是:</p> <ul> <li><code>--colors</code> 输出结果带彩色,比如:会用红色显示耗时较长的步骤</li> <li><code>--profile</code> 输出性能数据,可以看到每一步的耗时</li> <li><code>--display-modules</code> 默认情况下 <code>node_modules</code> 下的模块会被隐藏,加上这个参数可以显示这些被隐藏的模块</li> </ul> <p>这次命令行的结果已经很有参考价值,可以帮助我们定位耗时比较长的步骤</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Hash: bf9007fb1e0cb30e3ef7 Version: webpack 1.10.0 Time: 650ms Asset Size Chunks Chunk Names bundle.js 378 kB 0 [emitted] null [0] ./entry.js 125 bytes {0} [built] factory:11ms building:8ms = 19ms [1] ../~/moment/moment.js 102 kB {0} [built] [0] 19ms -&gt; factory:7ms building:141ms = 167ms [2] (webpack)/buildin/module.js 251 bytes {0} [built] [0] 19ms -&gt; [1] 148ms -&gt; factory:132ms building:159ms = 458ms [3] ../~/moment/locale ^\.\/.*$ 2.01 kB {0} [optional] [built] [0] 19ms -&gt; [1] 148ms -&gt; factory:6ms building:10ms dependencies:113ms = 296ms [4] ../~/moment/locale/af.js 2.57 kB {0} [optional] [built] [0] 19ms -&gt; [1] 148ms -&gt; [3] 16ms -&gt; factory:52ms building:65ms dependencies:138ms = 438ms ..... 广告分割线,Node.js 工程师简历请发 nodejs@oneapm.com ...... [85] ../~/moment/locale/zh-cn.js 4.31 kB {0} [optional] [built] [0] 22ms -&gt; [1] 162ms -&gt; [3] 18ms -&gt; factory:125ms building:145ms dependencies:22ms = 494ms [86] ../~/moment/locale/zh-tw.js 3.07 kB {0} [optional] [built] [0] 22ms -&gt; [1] 162ms -&gt; [3] 18ms -&gt; factory:126ms building:146ms dependencies:21ms = 495ms </code></pre></div> <p>从命令行的结果里可以看到从 Request[4] 到 Request[86] 都是在解析 <code>moment.js</code> 附带的大量本地化文件。所以我们遇到的速度慢的问题其实是由 <code>moment</code> 引起的。</p> <p>如果你想知道为什么 Webpack 会加载这么多的模块,可以参考这篇文章 <a href="https://github.com/wyvernnot/webpack_performance/tree/master/moment-example/WHY_LOCALES.md"> Why Enormous Locales During Webpack MomentJS</a></p> <p>我们再来看看 <code>entry.js</code> 代码的第一行,标准的 <code>CommonJS</code> 写法:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">moment</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;moment&#39;</span><span class="p">);</span> </code></pre></div> <p>也就是说,请求的是 <code>moment</code> 的源码。实际上,通过 NPM 安装 <code>moment</code> 的时候会同时安装 <code>moment</code> 的源码和压缩后的代码,试验证明下面这种写法也是可行的:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">moment</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;moment/min/moment-with-locales.min.js&#39;</span><span class="p">);</span> </code></pre></div> <p>只不过这样改,可读性会有所下降,而且每一个用到 <code>moment</code> 的地方都得这么写。另外,如果同样的问题出现在第三方模块中,修改别人代码就不那么方便了。下面来看看用 Webpack 怎么解决这个问题。</p> <h3>在 Webpack 中使用别名</h3> <p>别名(<code>resolve.alias</code>) 是 Webpack 的一个配置项,它的作用是把用户的一个请求重定向到另一个路径,例如通过修改 <code>webpack.config.js</code> 配置文件,加入:</p> <div class="highlight"><pre><code class="language-text" data-lang="text"> resolve: { alias: { moment: &quot;moment/min/moment-with-locales.min.js&quot; } } </code></pre></div> <p>这样待打包的脚本中的 <code>require(&#39;moment&#39;);</code> 其实就等价于 <code>require(&#39;moment/min/moment-with-locales.min.js&#39;);</code> 。通过别名的使用在本例中可以减少几乎一半的时间。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Hash: cdea65709b783ee0741a Version: webpack 1.10.0 Time: 320ms Asset Size Chunks Chunk Names bundle.js 148 kB 0 [emitted] main [0] ./entry.js 125 bytes {0} [built] factory:11ms building:9ms = 20ms [1] ../~/moment/min/moment-with-locales.min.js 146 kB {0} [built] [1 warning] [0] 20ms -&gt; factory:8ms building:263ms = 291ms [2] (webpack)/buildin/module.js 251 bytes {0} [built] [0] 20ms -&gt; [1] 271ms -&gt; factory:3ms building:1ms = 295ms WARNING in ../~/moment/min/moment-with-locales.min.js Module not found: Error: Cannot resolve &#39;file&#39; or &#39;directory&#39; ./locale in */webpack_performance/node_modules/moment/min @ ../~/moment/min/moment-with-locales.min.js 1:2731-2753 </code></pre></div> <h3>在 Webpack 中忽略对已知文件的解析</h3> <p><code>module.noParse</code> 是 <code>webpack</code> 的另一个很有用的配置项,如果你 <strong>确定一个模块中没有其它新的依赖</strong> 就可以配置这项,<code>webpack</code> 将不再扫描这个文件中的依赖。</p> <div class="highlight"><pre><code class="language-text" data-lang="text"> module: { noParse: [/moment-with-locales/] } </code></pre></div> <p>这样修改,再结合前面重命名的例子,更新后的流程是: </p> <ul> <li><code>webpack</code> 检查到 <code>entry.js</code> 文件对 <code>moment</code> 的请求;</li> <li>请求被 <code>alias</code> 重定向,转而请求 <code>moment/min/moment-with-locales.min.js</code>;</li> <li><code>noParse</code> 规则中的 <code>/moment-with-locales/</code> 一条生效,所以 <code>webpack</code> 就直接把依赖打包进了 <code>bundle.js</code> 。</li> </ul> <div class="highlight"><pre><code class="language-text" data-lang="text">Hash: 907880ed7638b4ed70b9 Version: webpack 1.10.0 Time: 76ms Asset Size Chunks Chunk Names bundle.js 147 kB 0 [emitted] main [0] ./entry.js 125 bytes {0} [built] factory:13ms building:13ms = 26ms [1] ../~/moment/min/moment-with-locales.min.js 146 kB {0} [built] [0] 26ms -&gt; factory:13ms building:5ms = 44ms </code></pre></div> <p>时间进一步被压缩,只需要 76ms,比前一步还减少了 75%。</p> <h3>在 Webpack 中使用公用 CDN</h3> <p>Webpack 是如此的强大,用其打包的脚本可以运行在多种环境下,Web 环境只是其默认的一种,也是最常用的一种。考虑到 Web 上有很多的公用 CDN 服务,那么 怎么将 Webpack 和公用的 CDN 结合使用呢?方法是使用 <code>externals</code> 声明一个外部依赖。</p> <div class="highlight"><pre><code class="language-text" data-lang="text"> externals: { moment: true } </code></pre></div> <p>当然了 HTML 代码里需要加上一行</p> <div class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">&quot;//apps.bdimg.com/libs/moment/2.8.3/moment-with-locales.min.js&quot;</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div> <p>这次打包,结果只用了 49 ms,几乎达到了极限。</p> <h2>总结</h2> <p>本文结合本地时钟的例子,展示了定位 Webpack 性能问题的步骤,以及所需要的两个参数 :<code>--display-modules</code> 和 <code>--profile</code>。然后,重点介绍了 <code>resolve.alias</code> 即利用别名做重定向的方法和场景,在此基础上,配合 <code>module.noParse</code> 忽略某些模块的解析可以进一步加快速度。最后介绍了用 <code>externals</code> 定义外部依赖方法来使用公用 CDN。</p> <h3>关于</h3> <p>本文相关的源码在: <a href="https://github.com/wyvernnot/webpack_performance/tree/master/moment-example">https://github.com/wyvernnot/webpack_performance/tree/master/moment-example</a>;</p> <blockquote> <p>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创 ,想阅读更多技术文章,请访问<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM官方技术博客</a>。</p> </blockquote> Ruby Profiler详解之stackprof /ruby/2015/07/06/stack-prof/ Mon, 06 Jul 2015 00:00:00 +0000 /ruby/2015/07/06/stack-prof <h2>简介</h2> <p>stackprof是基于采样的一个调优工具,采样有什么好处呢?好处就是你可以线上使用,按照内置的算法抓取一部分数据,只影响一小部分性能。它会产生一系列的dump文件,然后你在线下分析这些文件,从而定位出问题,google有一篇基于采样的论文,也基本证明了采样是可行的。而stackprof也是深受google的perftools的影响,采用了采样的方式来做调优。</p> <h2>基本使用方法</h2> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">StackProf</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="ss">mode</span><span class="p">:</span> <span class="ss">:cpu</span><span class="p">,</span> <span class="ss">out</span><span class="p">:</span> <span class="s1">&#39;./stackprof.dump&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="c1"># 你的代码</span> <span class="k">end</span> </code></pre></div> <p>这里我们给出一段示例代码,来作为测试目标:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">&quot;stackprof&quot;</span> <span class="k">class</span> <span class="nc">Compute</span> <span class="k">def</span> <span class="nf">m1</span> <span class="s2">&quot;string&quot;</span> <span class="o">*</span> <span class="mi">100</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">m2</span> <span class="s2">&quot;string&quot;</span> <span class="o">*</span> <span class="mi">10000</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">start</span> <span class="mi">100_000</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span> <span class="n">m1</span> <span class="n">m2</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="no">StackProf</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="ss">mode</span><span class="p">:</span> <span class="ss">:cpu</span><span class="p">,</span> <span class="ss">out</span><span class="p">:</span> <span class="s1">&#39;./stackprof.dump&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="no">Compute</span><span class="o">.</span><span class="n">new</span><span class="o">.</span><span class="n">start</span> <span class="k">end</span> </code></pre></div> <p>保存为test.rb,同时执行<code>ruby test.rb</code>就会在当前目录下生成stackprof.dump文件,我们用stackprof打开这个文件:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">stackprof stackprof.dump --text </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">================================== Mode: cpu(1000) Samples: 1793 (0.61% miss rate) GC: 587 (32.74%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 1106 (61.7%) 1106 (61.7%) Compute#m2 98 (5.5%) 98 (5.5%) Compute#m1 1206 (67.3%) 2 (0.1%) block in Compute#start 1206 (67.3%) 0 (0.0%) &lt;main&gt; 1206 (67.3%) 0 (0.0%) Compute#start 1206 (67.3%) 0 (0.0%) &lt;main&gt; 1206 (67.3%) 0 (0.0%) block in &lt;main&gt; </code></pre></div> <p>这里可以很明显的看出是m2方法比较慢,占据了大部分的执行时间,相比其他的调优工具,它只是列出了用户自己的方法所占时间比,在ruby-prof中的测试中,它是会显示<code>String#*</code>这个方法的占比的,但是对于我们来说,它的意义不大,而stackprof是不会理会标准库里的方法的。同时stackprof也是可以过滤方法的,比如我们发现了m2这个方法有问题,那么就可以把它过滤出来,看看细节:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">stackprof stackprof.dump --text --method &#39;Compute#m2&#39; Compute#m2 (/Users/lizhe/Workspace/ruby-performance-tuning/test.rb:9) samples: 1106 self (61.7%) / 1106 total (61.7%) callers: 1106 ( 100.0%) block in Compute#start code: | 9 | end 1106 (61.7%) / 1106 (61.7%) | 10 | | 11 | def start </code></pre></div> <p>我们可以看到m2这个方法定义在哪一个文件的哪一行,同时是谁调用了它,以及还显示了它在源码中的上下文。假如有多个方法调用了m2,还会显示出这几个方法,以及他们调用m2所占的比例,也就是上面的callers部分,因为只有一个start方法调用了m2,所以它是100%。</p> <h2>在rack中的使用方法</h2> <p>stackprof本身实现了一个rack middleware,所以可以很方便的挂载到一个rack应用中:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">use</span> <span class="no">StackProf</span><span class="o">::</span><span class="no">Middleware</span><span class="p">,</span> <span class="ss">enabled</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">mode</span><span class="p">:</span> <span class="ss">:cpu</span><span class="p">,</span> <span class="ss">save_every</span><span class="p">:</span> <span class="mi">5</span> </code></pre></div> <p>在rails中使用,先在Gemfile中添加stackprof,然后添加middleware:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">config</span><span class="o">.</span><span class="n">middleware</span><span class="o">.</span><span class="n">use</span> <span class="no">StackProf</span><span class="o">::</span><span class="no">Middleware</span><span class="p">,</span> <span class="ss">enabled</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">mode</span><span class="p">:</span> <span class="ss">:cpu</span><span class="p">,</span> <span class="ss">save_every</span><span class="p">:</span> <span class="mi">5</span> </code></pre></div> <p>然后请求你的应用,多请求几次,每5秒钟它会保存一次输出结果到tmp目录中,查看其中某一个结果:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">================================== Mode: cpu(1000) Samples: 155 (0.00% miss rate) GC: 11 (7.10%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 18 (11.6%) 18 (11.6%) Hike::Index#entries 12 (7.7%) 12 (7.7%) Hike::Index#stat 9 (5.8%) 9 (5.8%) #&lt;Module:0x007fb72a0c7b08&gt;.load_with_autoloading 18 (11.6%) 9 (5.8%) Sprockets::Cache::FileStore#[] 6 (3.9%) 6 (3.9%) block (2 levels) in BindingOfCaller::BindingExtensions#callers 5 (3.2%) 5 (3.2%) Time.parse 5 (3.2%) 5 (3.2%) Sprockets::Mime#mime_types 5 (3.2%) 5 (3.2%) Pathname#chop_basename 4 (2.6%) 4 (2.6%) block in ActionView::PathResolver#find_template_paths 4 (2.6%) 4 (2.6%) block in BetterErrors::ExceptionExtension#set_backtrace 15 (9.7%) 3 (1.9%) block in ActiveSupport::Dependencies#load_file 2 (1.3%) 2 (1.3%) Temple::Mixins::CompiledDispatcher::DispatchNode#initialize 5 (3.2%) 2 (1.3%) ActionDispatch::Cookies::EncryptedCookieJar#initialize 2 (1.3%) 2 (1.3%) ActiveSupport::KeyGenerator#generate_key 2 (1.3%) 2 (1.3%) block in ActionView::PathResolver#query 4 (2.6%) 2 (1.3%) Slim::Parser#initialize 113 (72.9%) 2 (1.3%) ActionView::Renderer#render_template 2 (1.3%) 2 (1.3%) Hike::Trail#stat 2 (1.3%) 2 (1.3%) block in ActiveSupport::Dependencies#search_for_file 22 (14.2%) 2 (1.3%) block in Temple::Filters::MultiFlattener#on_multi 20 (12.9%) 2 (1.3%) Temple::Filters::ControlFlow#dispatcher 15 (9.7%) 2 (1.3%) ActionView::Renderer#render_partial 1 (0.6%) 1 (0.6%) block in Slim::Parser#initialize 1 (0.6%) 1 (0.6%) Pathname#prepend_prefix 1 (0.6%) 1 (0.6%) String#blank? 1 (0.6%) 1 (0.6%) ActiveSupport::SafeBuffer#initialize 10 (6.5%) 1 (0.6%) Sprockets::Asset#dependency_fresh? 1 (0.6%) 1 (0.6%) Sprockets::Asset#init_with 1 (0.6%) 1 (0.6%) Hike::Index#sort_matches 1 (0.6%) 1 (0.6%) block in ActiveSupport::Dependencies::Loadable#require </code></pre></div> <p>可以利用这样的方式调试线上的环境。</p> <p>参考链接:</p> <p><a href="https://github.com/tmm1/stackprof">https://github.com/tmm1/stackprof</a></p> <p><strong>本文由<a href="http://oneapm.com/index.html?utm_source=RubyChina&amp;utm_medium=Post&amp;utm_content=lizhe&amp;utm_campaign=TechnicalPost&amp;from=opcofisiru">OneAPM</a>工程师原创,欢迎大家来<a href="http://oneapm.com/index.html?utm_source=RubyChina&amp;utm_medium=Post&amp;utm_content=lizhe&amp;utm_campaign=TechnicalPost&amp;from=opcofisiru">OneAPM</a>做客,共同讨论各种技术问题</strong></p> 如何测试你自己的 RubyGem? /ruby/2015/07/03/how-to-test-a-gem/ Fri, 03 Jul 2015 00:00:00 +0000 /ruby/2015/07/03/how-to-test-a-gem <h2>如何测试一个Gem</h2> <p>gem开发完了,想要给别人用,那就需要测试啊,测试一个gem其实很简单,这里我们用minitest为例,rspec也一样适用。先来看看我们当前这个gem的目录结构:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">-rw-rw-r-- 1 lizhe lizhe 90 7月 2 15:52 Gemfile -rw-rw-r-- 1 lizhe lizhe 379 7月 3 10:09 Gemfile.lock drwxrwxr-x 3 lizhe lizhe 4096 7月 2 15:52 lib -rw-rw-r-- 1 lizhe lizhe 1062 7月 2 15:52 LICENSE.txt -rw-rw-r-- 1 lizhe lizhe 923 7月 3 10:09 mygem.gemspec drwxrwxr-x 2 lizhe lizhe 4096 7月 2 18:33 pkg -rw-rw-r-- 1 lizhe lizhe 187 7月 3 10:35 Rakefile -rw-rw-r-- 1 lizhe lizhe 556 7月 2 15:52 README.md </code></pre></div> <p>打开mygem.gemspec,添加<code>minitest</code>:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">spec</span><span class="o">.</span><span class="n">add_development_dependency</span> <span class="s2">&quot;minitest&quot;</span><span class="p">,</span> <span class="s2">&quot;~&gt; 5.7.0&quot;</span> </code></pre></div> <p>执行<code>bundle install</code>安装<code>minitest</code>。</p> <p>新建一个<code>test</code>文件夹,存放我们的测试的用例,然后新建一个<code>test_helper.rb</code>文件,放在里面。<code>test_helper.rb</code>的内容如下:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="vg">$LOAD_PATH</span> <span class="o">&lt;&lt;</span> <span class="s2">&quot;./lib&quot;</span> <span class="c1"># 把lib添加到load path</span> <span class="nb">require</span> <span class="s1">&#39;minitest/autorun&#39;</span> <span class="c1"># 引进minitest</span> <span class="nb">require</span> <span class="s1">&#39;mygem&#39;</span> </code></pre></div> <p>再来新建一个测试用例,<code>test_mygem.rb</code>:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">&quot;test_helper&quot;</span> <span class="k">class</span> <span class="nc">MygemTest</span> <span class="o">&lt;</span> <span class="no">Minitest</span><span class="o">::</span><span class="no">Test</span> <span class="k">def</span> <span class="nf">test_hello_output</span> <span class="n">assert_equal</span><span class="p">(</span><span class="no">Mygem</span><span class="o">.</span><span class="n">hello</span><span class="p">,</span> <span class="s2">&quot;hello from my gem&quot;</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>现在就来执行测试吧:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ ruby test/test_mygem.rb /home/lizhe/.rvm/rubies/ruby-2.1.5/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:54:in `require&#39;: cannot load such file -- test_helper (LoadError) from /home/lizhe/.rvm/rubies/ruby-2.1.5/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:54:in `require&#39; from test/test_mygem.rb:1:in `&lt;main&gt;&#39; </code></pre></div> <p>出错了!找不到<code>test_helper</code>,因为它没有在加载路径里嘛,那就来换个方式,<code>require_relative &#39;test_helper&#39;</code>,因为我们的命令是在gem根目录下的,所以相对路径就是当前的路径,如果是在test目录下执行,就需要写成<code>require_relative &#39;../test_helper&#39;</code>了,还挺麻烦。好,执行一下试一试:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ ruby test/test_mygem.rb Run options: --seed 30741 # Running: . Finished in 0.000793s, 1260.9959 runs/s, 1260.9959 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips </code></pre></div> <h2>利用Rake::TestTask简化测试流程</h2> <p>前面的测试方法中,我们要手动添加lib目录到load path,然后在每个测试用例文件中要<code>require_relative &#39;test_helper&#39;</code>,很是麻烦,现在来简化这一个流程。</p> <p>首先添加<code>Rake::TestTask</code>到<code>Rakefile</code>中:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">&#39;rake/testtask&#39;</span> <span class="no">Rake</span><span class="o">::</span><span class="no">TestTask</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="o">.</span><span class="n">libs</span> <span class="o">&lt;&lt;</span> <span class="s1">&#39;test&#39;</span> <span class="o">&lt;&lt;</span> <span class="s1">&#39;lib&#39;</span> <span class="n">t</span><span class="o">.</span><span class="n">pattern</span> <span class="o">=</span> <span class="s2">&quot;test/test_*.rb&quot;</span> <span class="k">end</span> </code></pre></div> <p>现在把test<em>helper中的`$LOAD</em>PATH &lt;&lt; &#39;./lib&#39;<code>去掉,再把测试用例文件中的</code>require_relative<code>替换为</code>require<code>,因为rak test task已经把test和lib两个目录都添加到load path中了,然后执行</code>rake test`:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ rake test Run options: --seed 29947 # Running: . Finished in 0.000969s, 1031.6447 runs/s, 1031.6447 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips </code></pre></div> <p>进一步简化,每个测试用例文件都要<code>require &#39;test_helper&#39;</code>,也是够麻烦的,能不能让它自动执行这个动作呢?可以,只需要再加上一个选项即可:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">&#39;rake/testtask&#39;</span> <span class="no">Rake</span><span class="o">::</span><span class="no">TestTask</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="o">.</span><span class="n">libs</span> <span class="o">&lt;&lt;</span> <span class="s1">&#39;test&#39;</span> <span class="o">&lt;&lt;</span> <span class="s1">&#39;lib&#39;</span> <span class="n">t</span><span class="o">.</span><span class="n">pattern</span> <span class="o">=</span> <span class="s2">&quot;test/test_*.rb&quot;</span> <span class="n">t</span><span class="o">.</span><span class="n">ruby_opts</span> <span class="o">&lt;&lt;</span> <span class="s2">&quot;-r test_helper&quot;</span> <span class="c1"># 添加ruby运行参数,require指定的文件</span> <span class="k">end</span> </code></pre></div> <p>现在把测试用例中的<code>require &#39;test_helper&#39;</code>这一行也去掉,执行<code>rake test</code>,同样可以运行测试,又少写了一行,:smile:</p> <p>现在来设置默认的task:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">&#39;rake/testtask&#39;</span> <span class="no">Rake</span><span class="o">::</span><span class="no">TestTask</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="o">.</span><span class="n">libs</span> <span class="o">&lt;&lt;</span> <span class="s1">&#39;test&#39;</span> <span class="o">&lt;&lt;</span> <span class="s1">&#39;lib&#39;</span> <span class="n">t</span><span class="o">.</span><span class="n">pattern</span> <span class="o">=</span> <span class="s2">&quot;test/test_*.rb&quot;</span> <span class="n">t</span><span class="o">.</span><span class="n">ruby_opts</span> <span class="o">&lt;&lt;</span> <span class="s2">&quot;-r test_helper&quot;</span> <span class="c1"># 添加ruby运行参数,require指定的文件</span> <span class="k">end</span> <span class="n">task</span> <span class="ss">:default</span> <span class="o">=&gt;</span> <span class="ss">:test</span> </code></pre></div> <p>这样我就可以直接执行<code>rake</code>就可以跑测试了,连那个<code>test</code>都省了。</p> <p>如果我们有多个测使用例,这个rake test task会跑所有测试,如果想跑指定的某一个怎么做呢?指定一个TEST参数即可:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">rake test TEST=test/test_mygem.rb </code></pre></div> <p>参考链接:</p> <ul> <li><a href="http://ruby-doc.org/stdlib-2.2.2/libdoc/rake/rdoc/Rake/TestTask.html">Rake::TestTask</a></li> </ul> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> MariaDB Galera Cluster 部署 /database/2015/07/02/mariadb-galera-cluster/ Thu, 02 Jul 2015 00:00:00 +0000 /database/2015/07/02/mariadb-galera-cluster <p>MariaDB作为Mysql的一个分支,在开源项目中已经广泛使用,例如大热的openstack,所以,为了保证服务的高可用性,同时提高系统的负载能力,集群部署是必不可少的。</p> <h4>MariaDB Galera Cluster 介绍</h4> <p>MariaDB集群是MariaDB同步多主机集群。它仅支持XtraDB/ InnoDB存储引擎(虽然有对MyISAM实验支持 - 看wsrep_replicate_myisam系统变量)。</p> <p>主要功能:</p> <ul> <li>同步复制</li> <li>真正的multi-master,即所有节点可以同时读写数据库</li> <li>自动的节点成员控制,失效节点自动被清除</li> <li>新节点加入数据自动复制</li> <li>真正的并行复制,行级</li> <li>用户可以直接连接集群,使用感受上与MySQL完全一致</li> </ul> <p>优势:</p> <ul> <li>因为是多主,所以不存在Slavelag(延迟)</li> <li>不存在丢失事务的情况</li> <li>同时具有读和写的扩展能力</li> <li>更小的客户端延迟</li> <li>节点间数据是同步的,而Master/Slave模式是异步的,不同slave上的binlog可能是不同的</li> </ul> <p>技术:</p> <p>Galera集群的复制功能基于Galeralibrary实现,为了让MySQL与Galera library通讯,特别针对MySQL开发了wsrep API。</p> <p>Galera插件保证集群同步数据,保持数据的一致性,靠的就是可认证的复制,工作原理如下图: <img src="http://7xk2is.com1.z0.glb.clouddn.com/mariadb-galera-cluster.png" alt=""></p> <p>当客户端发出一个commit的指令,在事务被提交之前,所有对数据库的更改都会被 <code>write-set</code> 收集起来,并且将 <code>write-set</code> 纪录的内容发送给其他节点。</p> <p><code>write-set</code> 将在每个节点进行认证测试,测试结果决定着节点是否应用<code>write-set</code>更改数据。</p> <p>如果认证测试失败,节点将丢弃 <code>write-set</code> ;如果认证测试成功,则事务提交。</p> <h6>1 安装环境准备</h6> <p>安装MariaDB集群至少需要3台服务器(如果只有两台的话需要特殊配置,请参照<a href="http://galeracluster.com/documentation-webpages/twonode.html">官方文档</a>)</p> <p>在这里,我列出试验机器的配置:</p> <p>操作系统版本:centos7</p> <p>node4:10.128.20.16 node5:10.128.20.17 node6:10.128.20.18</p> <p>以第一行为例,node4为 <code>hostname</code> ,10.128.20.16为 <code>ip</code> ,在三台机器修改 <code>/etc/hosts</code> 文件,我的文件如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">10.128.20.16 node4 10.128.20.17 node5 10.128.20.18 node6 </code></pre></div> <p>为了保证节点间相互通信,需要禁用防火墙设置(如果需要防火墙,则参照<a href="http://galeracluster.com/documentation-webpages/galerainstallation.html#firewall-configuration">官方网站</a>增加防火墙信息设置)</p> <p>在三个节点分别执行命令:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">systemctl stop firewalld </code></pre></div> <p>然后将 <code>/etc/sysconfig/selinux</code> 的 <code>selinux</code> 设置成 <code>disabled</code> ,这样初始化环境就完成了。</p> <h6>2 安装 MariaDB Galera Cluster</h6> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 ~]# yum install -y mariadb mariadb-galera-server mariadb-galera-common galera rsync </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">[root@node5 ~]# yum install -y mariadb mariadb-galera-server mariadb-galera-common galera rsync </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">[root@node6 ~]# yum install -y mariadb mariadb-galera-server mariadb-galera-common galera rsync </code></pre></div> <h6>3 配置 MariaDB Galera Cluster</h6> <p>初始化数据库服务,只在一个节点进行</p> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 mariadb]# systemctl start mariadb [root@node4 mariadb]# mysql_secure_installation NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY! In order to log into MariaDB to secure it, we&#39;ll need the current password for the root user. If you&#39;ve just installed MariaDB, and you haven&#39;t set the root password yet, the password will be blank, so you should just press enter here. Enter current password for root (enter for none): OK, successfully used password, moving on... Setting the root password ensures that nobody can log into the MariaDB root user without the proper authorisation. Set root password? [Y/n] New password: Re-enter new password: Password updated successfully! Reloading privilege tables.. ... Success! By default, a MariaDB installation has an anonymous user, allowing anyone to log into MariaDB without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. Remove anonymous users? [Y/n] n ... skipping. Normally, root should only be allowed to connect from &#39;localhost&#39;. This ensures that someone cannot guess at the root password from the network. Disallow root login remotely? [Y/n] y ... Success! By default, MariaDB comes with a database named &#39;test&#39; that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. Remove test database and access to it? [Y/n] n ... skipping. Reloading the privilege tables will ensure that all changes made so far will take effect immediately. Reload privilege tables now? [Y/n] y ... Success! Cleaning up... All done! If you&#39;ve completed all of the above steps, your MariaDB installation should now be secure. Thanks for using MariaDB! </code></pre></div> <p>关闭数据库,修改 <code>/etc/my.cnf.d/galera.cnf</code></p> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 mariadb]# systemctl stop mariadb </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 ~]# vim /etc/my.cnf.d/galera.cnf </code></pre></div> <p>修改以下内容:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">[mysqld] ...... wsrep_provider = /usr/lib64/galera/libgalera_smm.so wsrep_cluster_address = &quot;gcomm://node4,node5,node6&quot; wsrep_node_name = node4 wsrep_node_address=10.128.20.16 #wsrep_provider_options=&quot;socket.ssl_key=/etc/pki/galera/galera.key; socket.ssl_cert=/etc/pki/galera/galera.crt;&quot; </code></pre></div> <p>提示:如果不用ssl的方式认证的话,请把 <code>wsrep_provider_options</code> 注释掉。</p> <p>将此文件复制到node5、node6,注意要把 <code>wsrep_node_name</code> 和 <code>wsrep_node_address</code> 改成相应节点的 <code>hostname</code> 和 <code>ip</code>。</p> <h6>4 启动 MariaDB Galera Cluster 服务</h6> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 ~]# /usr/libexec/mysqld --wsrep-new-cluster --user=root &amp; </code></pre></div> <p>观察日志:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 ~]# tail -f /var/log/mariadb/mariadb.log 150701 19:54:17 [Note] WSREP: wsrep_load(): loading provider library &#39;none&#39; 150701 19:54:17 [Note] /usr/libexec/mysqld: ready for connections. Version: &#39;5.5.40-MariaDB-wsrep&#39; socket: &#39;/var/lib/mysql/mysql.sock&#39; port: 3306 MariaDB Server, wsrep_25.11.r4026 </code></pre></div> <p>出现 <code>ready for connections</code> ,证明我们启动成功,继续启动其他节点:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node5 ~]# systemctl start mariadb </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">[root@node6 ~]# systemctl start mariadb </code></pre></div> <p>可以查看 <code>/var/log/mariadb/mariadb.log</code>,在日志可以看到节点均加入了集群中。</p> <p>警告⚠:<code>--wsrep-new-cluster</code> 这个参数只能在初始化集群使用,且只能在一个节点使用。</p> <h6>5 查看集群状态</h6> <p><img src="http://7xk2is.com1.z0.glb.clouddn.com/galera-status.png" alt=""></p> <p>我们可以关注几个关键的参数:</p> <p><code>wsrep_connected = on</code> <em>链接已开启</em></p> <p><code>wsrep_local_index = 1</code> <em>在集群中的索引值</em></p> <p><code>wsrep_cluster_size =3</code> <em>集群中节点的数量</em></p> <p><code>wsrep_incoming_addresses = 10.128.20.17:3306,10.128.20.16:3306,10.128.20.18:3306</code> <em>集群中节点的访问地址</em></p> <h6>6 验证数据同步</h6> <p>我们在 <code>node4</code> 上新建数据库 <code>galera_test</code> ,然后在 <code>node5</code> 和 <code>node6</code> 上查询,如果可以查询到 <code>galera_test</code> 这个库,说明数据同步成功,集群运行正常。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">[root@node4 ~]# mysql -uroot -proot -e &quot;create database galera_test&quot; </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">[root@node5 ~]# mysql -uroot -proot -e &quot;show databases&quot; +--------------------+ | Database | +--------------------+ | information_schema | | galera_test | | mysql | | performance_schema | +--------------------+ </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">[root@node6 ~]# mysql -uroot -proot -e &quot;show databases&quot; +--------------------+ | Database | +--------------------+ | information_schema | | galera_test | | mysql | | performance_schema | +--------------------+ </code></pre></div> <p>至此,我们的 MariaDB Galera Cluster 已经成功部署。</p> <blockquote> <p>参考文章:</p> </blockquote> <ul> <li>http://galeracluster.com/documentation-webpages/</li> <li>https://mariadb.com/kb/en/mariadb/getting-started-with-mariadb-galera-cluster/</li> </ul> 如何开发一个自己的 RubyGem? /ruby/2015/07/02/how-to-create-a-gem/ Thu, 02 Jul 2015 00:00:00 +0000 /ruby/2015/07/02/how-to-create-a-gem <h2>什么是 RubyGem</h2> <p>RubyGem是Ruby语言的标准源码打包格式。</p> <p>大家一直都在用<code>gem</code>这个命令,但是很少有人知道这个东西是怎么来的,这里我从网上扒下一些资料汇总一下,分享给大家。最后面会有这些链接,想进一步了解的,可以点进去看看。Ruby语言深受其他几种脚本语言的影响,其中就有Perl,而Perl有个CPAN(Comprehensive Perl Archive Network),这个东西也就像是现在的RubyGems.org,但是当时Ruby是没有这样一个东西的。像CPAN和RubyGem,它们仅仅是定义好的一种源码的打包和安装方式,另外还有一些组织,会提供这种免费的公共的源码包的存储,这也就现在大家每天都要使用的安装方式:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">gem install rails </code></pre></div> <p>在RubyGem的发展历史当中,有几位重要的人物,这里也作为八卦知识给大家晒一晒,就当做大家茶余饭后的谈点吧。Ruby社区的人应该都知道Jim Weirich这个人,他已经在2014年2月份去世了,是一个可爱的白胡子大叔,他和另外的四位Rich Kilmer, Chad Fowler, David Black, Paul Brannan在2003年制定了RubyGem的基本规范和实现,但是其实RubyGem最早是Ryan Leavengood在2001年开发的,可惜没有流行起来,最后到了2003年,前面的5个人经过Ryan Leavengood的同意,使用了RubyGem这个名称,开发了新版的RubyGem,其中并没有使用Ryan Leavengood的代码。这里附上rubygems的执行文件链接,看看注释,里面有上面几个人的名字 <a href="https://github.com/rubygems/rubygems/blob/master/bin/gem">rubygems/blob/master/bin/gem</a></p> <p>rubygems有默认的源,也可以更改,国内的基本就是<code>https://rubygems.taobao.org</code>了,有些公司有自己的需求,也会搭建自己的私源。当前的官方源为<code>https://rubygems.org</code>,这个源也是几经辗转,早期的Ruby用户都知道<code>http://gems.rubyforge.org</code>和<code>http://gemcutter.org</code>,甚至github都曾经作为源使用过,也就是<code>http://gems.github.com</code>,这三个现在都已经弃用了。</p> <p>看看,一个简单的<code>gem install</code>历史还不少啊。</p> <h2>RubyGem的基本使用方法</h2> <div class="highlight"><pre><code class="language-text" data-lang="text">gem install rails //安装rails gem install rails -v 4.2.0 //安装指定版本的rails gem search rails //查找所有名称中含有rails的gem gem search ^rails //查找所有名称中以rails开头的gem gem search ^rails -d //查找所有名称中以rails开头的gem,并显示描述 gem build package.gemspec //构建一个gem,就是把你自己写的gem源码,打包成一个.gem文件 gem push pack-1.0.gem //发布到源上,默认是rubygems.org </code></pre></div> <p>这里只是简单列出了最常用的使用方法,大家看看就好,命令很有限,也很简单,执行<code>gem --help</code>,基本上所有的东西你都能10分钟内学会。</p> <h2>如何制作一个自己的RubyGem</h2> <p>前几年还是有这样那样的工具,现在用<code>bundler</code>就够了。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ bundler gem mygem create mygem/Gemfile create mygem/Rakefile create mygem/LICENSE.txt create mygem/README.md create mygem/.gitignore create mygem/mygem.gemspec create mygem/lib/mygem.rb create mygem/lib/mygem/version.rb Initializing git repo in /home/lizhe/Workspace/mygem </code></pre></div> <p>一个bundler命令就搞定了。来看看mygem这个文件夹下的东西:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">total 24 -rw-rw-r-- 1 lizhe lizhe 90 7月 2 15:52 Gemfile drwxrwxr-x 3 lizhe lizhe 4096 7月 2 15:52 lib -rw-rw-r-- 1 lizhe lizhe 1062 7月 2 15:52 LICENSE.txt -rw-rw-r-- 1 lizhe lizhe 850 7月 2 15:52 mygem.gemspec -rw-rw-r-- 1 lizhe lizhe 29 7月 2 15:52 Rakefile -rw-rw-r-- 1 lizhe lizhe 556 7月 2 15:52 README.md </code></pre></div> <p>现在来看看gemspec这个文件,它描述了这个Gem的各种信息</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># coding: utf-8</span> <span class="n">lib</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">expand_path</span><span class="p">(</span><span class="s1">&#39;../lib&#39;</span><span class="p">,</span> <span class="bp">__FILE__</span><span class="p">)</span> <span class="vg">$LOAD_PATH</span><span class="o">.</span><span class="n">unshift</span><span class="p">(</span><span class="n">lib</span><span class="p">)</span> <span class="k">unless</span> <span class="vg">$LOAD_PATH</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">lib</span><span class="p">)</span> <span class="nb">require</span> <span class="s1">&#39;mygem/version&#39;</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Specification</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">spec</span><span class="o">|</span> <span class="n">spec</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="s2">&quot;mygem&quot;</span> <span class="n">spec</span><span class="o">.</span><span class="n">version</span> <span class="o">=</span> <span class="no">Mygem</span><span class="o">::</span><span class="no">VERSION</span> <span class="n">spec</span><span class="o">.</span><span class="n">authors</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&quot;lizhe&quot;</span><span class="o">]</span> <span class="n">spec</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&quot;lizhe@oneapm.com&quot;</span><span class="o">]</span> <span class="n">spec</span><span class="o">.</span><span class="n">summary</span> <span class="o">=</span> <span class="sx">%q{TODO: Write a short summary. Required.}</span> <span class="n">spec</span><span class="o">.</span><span class="n">description</span> <span class="o">=</span> <span class="sx">%q{TODO: Write a longer description. Optional.}</span> <span class="n">spec</span><span class="o">.</span><span class="n">homepage</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span> <span class="n">spec</span><span class="o">.</span><span class="n">license</span> <span class="o">=</span> <span class="s2">&quot;MIT&quot;</span> <span class="n">spec</span><span class="o">.</span><span class="n">files</span> <span class="o">=</span> <span class="sb">`git ls-files -z`</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\x0</span><span class="s2">&quot;</span><span class="p">)</span> <span class="n">spec</span><span class="o">.</span><span class="n">executables</span> <span class="o">=</span> <span class="n">spec</span><span class="o">.</span><span class="n">files</span><span class="o">.</span><span class="n">grep</span><span class="p">(</span><span class="sr">%r{^bin/}</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="no">File</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">}</span> <span class="n">spec</span><span class="o">.</span><span class="n">test_files</span> <span class="o">=</span> <span class="n">spec</span><span class="o">.</span><span class="n">files</span><span class="o">.</span><span class="n">grep</span><span class="p">(</span><span class="sr">%r{^(test|spec|features)/}</span><span class="p">)</span> <span class="n">spec</span><span class="o">.</span><span class="n">require_paths</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&quot;lib&quot;</span><span class="o">]</span> <span class="n">spec</span><span class="o">.</span><span class="n">add_development_dependency</span> <span class="s2">&quot;bundler&quot;</span><span class="p">,</span> <span class="s2">&quot;~&gt; 1.7&quot;</span> <span class="n">spec</span><span class="o">.</span><span class="n">add_development_dependency</span> <span class="s2">&quot;rake&quot;</span><span class="p">,</span> <span class="s2">&quot;~&gt; 10.0&quot;</span> <span class="k">end</span> </code></pre></div> <p>我发现有人看到这个文件的内容时,倒是关心那个<code>&#39;git ls-files -z&#39;.split(&quot;\x0&quot;)</code>是什么意思?以及那个<code>\x0</code>是什么?附上一个链接,解释一下,<a href="http://stackoverflow.com/questions/25192836/what-does-the-ruby-method-split-x0-return">参考链接</a>。这个文件最上面先把lib文件夹添加到load path中,Gem::Specification的第一部分主要是描述这个gem的信息,包括名称,版本等等,第二部分是这个gem都包括哪些文件,执行文件,测试文件以及哪些路径下的文件可以添加到load path中。第三部分是开发mygem需要依赖的其他gem。这些信息都可以自定义,先按照默认走。让我们build第一个gem吧。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ rake build rake aborted! WARNING: See http://guides.rubygems.org/specification-reference/ for help ERROR: While executing gem ... (Gem::InvalidSpecificationException) &quot;FIXME&quot; or &quot;TODO&quot; is not a description /home/lizhe/.rvm/gems/ruby-2.1.5@global/gems/bundler-1.7.12/lib/bundler/gem_helper.rb:149:in `sh&#39; /home/lizhe/.rvm/gems/ruby-2.1.5@global/gems/bundler-1.7.12/lib/bundler/gem_helper.rb:57:in `build_gem&#39; /home/lizhe/.rvm/gems/ruby-2.1.5@global/gems/bundler-1.7.12/lib/bundler/gem_helper.rb:39:in `block in install&#39; /home/lizhe/.rvm/gems/ruby-2.1.5@global/bin/ruby_executable_hooks:15:in `eval&#39; /home/lizhe/.rvm/gems/ruby-2.1.5@global/bin/ruby_executable_hooks:15:in `&lt;main&gt;&#39; Tasks: TOP =&gt; build (See full trace by running task with --trace) </code></pre></div> <p>这个警告是提醒我们需要替换gemspec中的<code>FIXME</code>和<code>TODO</code>,这个警告如果不解决是无法build一个gem的,直接删除掉summary和description中的<code>TODO</code>:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"> <span class="n">spec</span><span class="o">.</span><span class="n">summary</span> <span class="o">=</span> <span class="sx">%q{Write a short summary. Required.}</span> <span class="n">spec</span><span class="o">.</span><span class="n">description</span> <span class="o">=</span> <span class="sx">%q{Write a longer description. Optional.}</span> </code></pre></div> <p>再来执行:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ rake build mygem 0.0.1 built to pkg/mygem-0.0.1.gem. </code></pre></div> <p>好了,第一个gem诞生了。它就在当前目录的pkg下:mygem-0.0.1.gem。如何使用呢?不考虑bundler的情况下,如果你开起了一个irb或者pry的session时,一般都会这样写:<code>require &quot;mygem&quot;</code>,如果你现在这样做,那肯定不行,因为它还没有被安装到ruby的load path中,那就把它安装上。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ rake install mygem 0.0.1 built to pkg/mygem-0.0.1.gem. mygem (0.0.1) installed. </code></pre></div> <p>安装好了,那就来使用一下,打开irb:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">require &quot;mygem&quot; =&gt; true Mygem =&gt; Mygem </code></pre></div> <p>看,已经可以使用这个module了,不过这个gem啥也干不了,那么我们就给它添加一个方法吧,打开lib/mygem.rb,添加一个方法:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">&quot;mygem/version&quot;</span> <span class="k">module</span> <span class="nn">Mygem</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">hello</span> <span class="nb">p</span> <span class="s2">&quot;hello from my gem&quot;</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>保存,然后执行<code>rake install</code>,这个命令会先build然后install,再重新打开irb:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">require &quot;mygem&quot; =&gt; true Mygem.hello =&gt; &quot;hello from my gem&quot; </code></pre></div> <p>能够正常运行了,那就来发布第一个gem吧:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">rake release // 输入你在rubygems.org上的账号和密码 </code></pre></div> <p>如果你的一个rails应用正好需要输出一个<code>hello from my gem</code>,那么现在你可以在Gemfile中添加这个gem了:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">gem</span> <span class="s1">&#39;mygem&#39;</span> </code></pre></div> <p>添加完执行<code>bundle install</code>,你就可以在你的rails应用中使用它了。</p> <p>参考链接:</p> <ul> <li><a href="http://www.linuxjournal.com/article/8967">RubyGems</a></li> <li><a href="http://www.rubyinside.com/gemcutter-is-the-new-official-default-rubygem-host-2659.html">Gemcutter Is The New Official Default RubyGem Host</a></li> <li><a href="https://github.com/blog/515-gem-building-is-defunct">Gem Building is Defunct</a></li> </ul> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 动态修改 NodeJS 程序中的变量值 /nodejs/2015/06/27/intereference/ Sat, 27 Jun 2015 00:00:00 +0000 /nodejs/2015/06/27/intereference <p>如果一个 NodeJS 进程正在运行,有办法修改程序中的变量值么?答案是:通过 V8 的 Debugger 接口可以!本文将详细介绍实现步骤。</p> <h2>启动一个 HTTP Server</h2> <p>用简单的 Hello World 做例子吧,不过略作修改。在 <code>global</code> 下放一个变量 <code>message</code>, 然后打印出来:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="c1">// message content will be modified !</span> <span class="nx">global</span><span class="p">.</span><span class="nx">message</span> <span class="o">=</span> <span class="s2">&quot;hello world!&quot;</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;http&#39;</span><span class="p">).</span><span class="nx">createServer</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span> <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="nx">global</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span> <span class="p">}).</span><span class="nx">listen</span><span class="p">(</span><span class="mi">8001</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;pid = %d&#39;</span><span class="p">,</span> <span class="nx">process</span><span class="p">.</span><span class="nx">pid</span><span class="p">);</span> </code></pre></div> <p>用命令启动 Server,此时,通过用浏览器访问 <code>http://localhost:8001</code> 可以看到网页内容是 <code>hello world!</code>。 接下来我们将尝试在不改变代码,不重启进程的情况下把 <code>message</code> 换成 &quot;hello bugs!&quot;。</p> <h2>使 Server 进程进入 Debug 模式</h2> <p>V8 引擎在实现的时候留了 Debugger 接口。 通过命令 <code>node --debug-brk=5858 [filename]</code> 可以启动一个脚本,并且立即进入 Debug 模式。</p> <p>那么如果是已经运行着的 NodeJS 程序,可以进入 Debug 模式吗?也是可以的,在操作系统下给 NodeJS 的进程发一个 <code>SIGUSR1</code> 信号,可以让进程进入 Debug 模式。 进入 Debug 模式的进程会在本地启动一个 TCP Server 并且默认监听 <code>5858</code> 端口。 </p> <p>此时在另一个命令行窗口执行命令 <code>node debug localhost:5858</code> 就可以连接到 Debugger 调试端口, 并且可以使用很多常用的 Debug 命令,比如 <code>c</code>继续执行,<code>s</code> 步入, <code>o</code>步出等。</p> <h2>Debugger 协议</h2> <p>使用 <code>node debug hostname:port</code> 命令连接到进程进行 Debug 的方式比较简单,但是要完成一些高级的功能就会处处受限,因为它只封装了 Debugger 协议中 command 的一部分。 下面介绍一下这个简单的协议。</p> <p>Client 和 Server 的通讯是通过 TCP 进行的。 DebugClient 第一次连接到 DebugServer 的时候会拿到一些 Header,比如 Node 版本, V8 版本等。后面紧跟着一个空的消息1</p> <p><strong>消息1</strong></p> <div class="highlight"><pre><code class="language-text" data-lang="text">Type: connect\r\n V8-Version: 3.28.71.19\r\n Protocol-Version: 1\r\n Embedding-Host: node v0.12.4\r\n Content-Length: 0\r\n \r\n </code></pre></div> <p>消息实体由 Header 和 Body 组成,消息1的 Body 为空,所以 Header 中对应的 Content-Length 为 0。而在下面这个例子里,Body 为一个单行的 JSON 字符串,这是由协议所规定的。</p> <p><strong>消息2</strong></p> <div class="highlight"><pre><code class="language-text" data-lang="text">Content-Length: 46\r\n \r\n {&quot;command&quot;:&quot;version&quot;,&quot;type&quot;:&quot;request&quot;,&quot;seq&quot;:1} </code></pre></div> <p>消息2的类型( type )是 <code>request</code>,代表这是 Client 发给 Server 的命令,其他的可能值是 <code>response</code> 和 <code>event</code> 分别代表 Server 对 Client 的相应,和 Server 端发生的事件。</p> <p><strong>消息3</strong></p> <div class="highlight"><pre><code class="language-text" data-lang="text">Content-Length: 137\r\n \r\n {&quot;seq&quot;:1,&quot;request_seq&quot;:1,&quot;type&quot;:&quot;response&quot;,&quot;command&quot;:&quot;version&quot;,&quot;success&quot;:true,&quot;body&quot;:{&quot;V8Version&quot;:&quot;3.28.71.19&quot;},&quot;refs&quot;:[],&quot;running&quot;:true} </code></pre></div> <p>消息2是 Client 发送给 Server的,消息3是 Server 对 Client 的相应,那么如何判断消息3是不是消息2的结果呢?可以看到消息2中的 seq 值是1,而 消息3中的 request_seq 值是1。 Debugger 协议正是通过这两个值把异步返回的结果和请求一一对应起来的。</p> <p>Debugger 协议就是这么的简单。</p> <h3>实例化一个 Debugger Client</h3> <p>了解了 Debugger 协议后,相信好奇心强的程序员已经跃跃欲试自己实现一个了。本着不重复发明轮子的原则开始在网上找实现,找了好久找到这个库 <a href="https://www.npmjs.com/package/pDebug">pDebug</a>, 可惜这个库已经好久不更新了。后来通过阅读 <code>node-inspector</code> 的源码才发现,其实 NodeJS 自带了一个 Debugger 模块, 相关代码在 <code>_debugger</code> 模块里(<a href="https://github.com/joyent/node/blob/master/lib/_debugger.js">源码</a>),由于模块名是以 <code>_</code> 开头的,所以网上找不到它的 API,好在代码注释写的非常详细,很快就能上手。</p> <p>我们需要的正是这个模块下的 Client, 而 Client 其实是继承于 Socket 的.</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">Client</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;_debugger&#39;</span><span class="p">).</span><span class="nx">Client</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Client</span><span class="p">();</span> <span class="nx">client</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span><span class="mi">5858</span><span class="p">);</span> <span class="nx">client</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;ready&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="c1">// 连接成功</span> <span class="p">});</span> </code></pre></div> <h3>通过 Debugger 接口执行命令</h3> <p>接下来我们来看看如何修改这个 global 的变量,代码如下</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">function</span> <span class="nx">modifyTheMessage</span><span class="p">(</span><span class="nx">newMessage</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">msg</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">&#39;command&#39;</span><span class="o">:</span> <span class="s1">&#39;evaluate&#39;</span><span class="p">,</span> <span class="s1">&#39;arguments&#39;</span><span class="o">:</span> <span class="p">{</span> <span class="s1">&#39;expression&#39;</span><span class="o">:</span> <span class="s1">&#39;global.message=&quot;&#39;</span> <span class="o">+</span> <span class="nx">newMessage</span> <span class="o">+</span> <span class="s1">&#39;&quot;&#39;</span><span class="p">,</span> <span class="s1">&#39;global&#39;</span><span class="o">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">};</span> <span class="nx">client</span><span class="p">.</span><span class="nx">req</span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">body</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;modified to %s&#39;</span><span class="p">,</span> <span class="nx">newMessage</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> </code></pre></div> <p><code>client.req</code> 方法封装了 <code>type=request</code> 消息类型 和 <code>seq</code> 自增的逻辑,因此在构造 <code>msg</code> JSON对象的时候不需要指明这两个属性。 我们要修改 message 其实就是在 JavaScript 调用的顶层执行 <code>global.message=newMessage</code></p> <h3>总结</h3> <p>此时,再访问 <code>http://localhost:8001</code> 可以看到网页上显示的内容已经由 <code>&#39;hello world!&#39;</code> 变成了 <code>&#39;hello bugs!&#39;</code>,是不是很神奇。</p> <p>这种方式也带来了很多可能性:</p> <ul> <li>动态修改配置</li> </ul> <p>线上的服务器不用重启就可以应用新的配置</p> <ul> <li>模块注入</li> </ul> <p>通过其他任意语言编写的应用程序为已经运行的 NodeJS 进程注入新的模块</p> <ul> <li>性能监控</li> </ul> <p>可以剥离用户线上代码对第三方性能监控模块的直接依赖</p> <ul> <li>错误监控</li> </ul> <p>发生异常时,通过 Debugger 可以抓到发生错误的函数和行号,并且抓取各个调用栈中的每一个变量,即使是在闭包里</p> <ul> <li>Chrome 调试</li> </ul> <p>由于 Chrome 也是基于 V8 的,上述方法也可以用于 Chrome 相关的功能集成</p> <h3>关于</h3> <p>1. 本文相关的源码在: <a href="https://github.com/wyvernnot/interference_demo">https://github.com/wyvernnot/interference_demo</a>;</p> <p>2. 如果你也对 Debugger 协议感兴趣,可以安装 oneapm-debugger 这个工具,它可以帮助你查看 Debug 过程中所有实际发送的数据。 <a href="https://www.npmjs.com/package/oneapm-debugger">oneapm-debugger</a></p> <blockquote> <p>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创 ,想阅读更多技术文章,请访问<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM官方技术博客</a>。</p> </blockquote> 【译】如何使用 Python 创建一个虚拟机解释器? /python/2015/06/19/python-vm/ Fri, 19 Jun 2015 00:00:00 +0000 /python/2015/06/19/python-vm <p>原文地址:<a href="https://csl.name/post/vm/">Making a simple VM interpreter in Python</a></p> <p><strong>更新:根据<a href="https://pay.reddit.com/r/Python/comments/35tg6b/making_a_simple_vm_interpreter_in_python/">大家的评论</a>我对代码做了轻微的改动。感谢 robin-gvx、 bs4h 和 Dagur,具体代码见<a href="https://github.com/cslarsen/python-simple-vm">这里</a></strong></p> <p>Stack Machine 本身并没有任何的寄存器,它将所需要处理的值全部放入堆栈中而后进行处理。Stack Machine 虽然简单但是却十分强大,这也是为神马 Python,Java,PostScript,Forth 和其他语言都选择它作为自己的虚拟机的原因。</p> <p>首先,我们先来谈谈堆栈。我们需要一个指令指针栈用于保存返回地址。这样当我们调用了一个子例程(比如调用一个函数)的时候我们就能够返回到我们开始调用的地方了。我们可以使用自修改代码(<a href="https://en.wikipedia.org/wiki/Self-modifying_code">self-modifying code</a>)来做这件事,恰如 Donald Knuth 发起的 <a href="https://en.wikipedia.org/wiki/MIX">MIX</a> 所做的那样。但是如果这么做的话你不得不自己维护堆栈从而保证递归能正常工作。在这篇文章中,我并不会真正的实现子例程调用,但是要实现它其实并不难(可以考虑把实现它当成练习)。</p> <p>有了堆栈之后你会省很多事儿。举个例子来说,考虑这样一个表达式 <code>(2+3)*4</code>。在 Stack Machine 上与这个表达式等价的代码为 <code>2 3 + 4 *</code>。首先,将 <code>2</code> 和 <code>3</code> 推入堆栈中,接下来的是操作符 <code>+</code>,此时让堆栈弹出这两个数值,再把它两加合之后的结果重新入栈。然后将 <code>4</code> 入堆,而后让堆栈弹出两个数值,再把他们相乘之后的结果重新入栈。多么简单啊!</p> <p>让我们开始写一个简单的堆栈类吧。让这个类继承 <code>collections.deque</code>:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">deque</span> <span class="k">class</span> <span class="nc">Stack</span><span class="p">(</span><span class="n">deque</span><span class="p">):</span> <span class="n">push</span> <span class="o">=</span> <span class="n">deque</span><span class="o">.</span><span class="n">append</span> <span class="k">def</span> <span class="nf">top</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> </code></pre></div> <p>现在我们有了 <code>push</code>、<code>pop</code> 和 <code>top</code> 这三个方法。<code>top</code> 方法用于查看栈顶元素。</p> <p>接下来,我们实现虚拟机这个类。在虚拟机中我们需要两个堆栈以及一些内存空间来存储程序本身(译者注:这里的程序请结合下文理解)。得益于 Pyhton 的动态类型我们可以往 list 中放入任何类型。唯一的问题是我们无法区分出哪些是字符串哪些是内置函数。正确的做法是只将真正的 Python 函数放入 list 中。我可能会在将来实现这一点。</p> <p>我们同时还需要一个指令指针指向程序中下一个要执行的代码。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">Machine</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">code</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span> <span class="o">=</span> <span class="n">Stack</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">return_addr_stack</span> <span class="o">=</span> <span class="n">Stack</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">=</span> <span class="mi">0</span> <span class="bp">self</span><span class="o">.</span><span class="n">code</span> <span class="o">=</span> <span class="n">code</span> </code></pre></div> <p>这时候我们增加一些方便使用的函数省得以后多敲键盘。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">pop</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="k">def</span> <span class="nf">push</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">def</span> <span class="nf">top</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="o">.</span><span class="n">top</span><span class="p">()</span> </code></pre></div> <p>然后我们增加一个 <code>dispatch</code> 函数来完成每一个操作码做的事儿(我们并不是真正的使用操作码,只是动态展开它,你懂的)。首先,增加一个解释器所必须的循环:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">while</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">):</span> <span class="n">opcode</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span><span class="p">]</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">+=</span> <span class="mi">1</span> <span class="bp">self</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">opcode</span><span class="p">)</span> </code></pre></div> <p>诚如您所见的,这货只好好的做一件事儿,即获取下一条指令,让指令指针执自增,然后根据操作码分别处理。<code>dispatch</code> 函数的代码稍微长了一点。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">op</span><span class="p">):</span> <span class="n">dispatch_map</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&quot;%&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mod</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mul</span><span class="p">,</span> <span class="s">&quot;+&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">plus</span><span class="p">,</span> <span class="s">&quot;-&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">minus</span><span class="p">,</span> <span class="s">&quot;/&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">div</span><span class="p">,</span> <span class="s">&quot;==&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eq</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cast_int</span><span class="p">,</span> <span class="s">&quot;cast_str&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cast_str</span><span class="p">,</span> <span class="s">&quot;drop&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">drop</span><span class="p">,</span> <span class="s">&quot;dup&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">dup</span><span class="p">,</span> <span class="s">&quot;if&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">if_stmt</span><span class="p">,</span> <span class="s">&quot;jmp&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">jmp</span><span class="p">,</span> <span class="s">&quot;over&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">over</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">print_</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">println</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">read</span><span class="p">,</span> <span class="s">&quot;stack&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">dump_stack</span><span class="p">,</span> <span class="s">&quot;swap&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">swap</span><span class="p">,</span> <span class="p">}</span> <span class="k">if</span> <span class="n">op</span> <span class="ow">in</span> <span class="n">dispatch_map</span><span class="p">:</span> <span class="n">dispatch_map</span><span class="p">[</span><span class="n">op</span><span class="p">]()</span> <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="nb">int</span><span class="p">):</span> <span class="c"># push numbers on the data stack</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">op</span><span class="p">)</span> <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="ow">and</span> <span class="n">op</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">==</span><span class="n">op</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">==</span><span class="s">&#39;&quot;&#39;</span><span class="p">:</span> <span class="c"># push quoted strings on the data stack</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">op</span><span class="p">[</span><span class="mi">1</span><span class="p">:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="k">else</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s">&quot;Unknown opcode: &#39;</span><span class="si">%s</span><span class="s">&#39;&quot;</span> <span class="o">%</span> <span class="n">op</span><span class="p">)</span> </code></pre></div> <p>基本上,这段代码只是根据操作码查找是都有对应的处理函数,例如 <code>*</code> 对应 <code>self.mul</code>,<code>drop</code> 对应 <code>self.drop</code>,<code>dup</code> 对应 <code>self.dup</code>。顺便说一句,你在这里看到的这段代码其实本质上就是简单版的 <a href="https://en.wikipedia.org/wiki/Forth_(programming_language)">Forth</a>。而且,Forth 语言还是值得您看看的。</p> <p>总之捏,它一但发现操作码是 <code>*</code> 的话就直接调用 <code>self.mul</code> 并执行它。就像这样:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">mul</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> </code></pre></div> <p>其他的函数也是类似这样的。如果我们在 <code>dispatch_map</code> 中查找不到相应操作函数,我们首先检查他是不是数字类型,如果是的话直接入栈;如果是被引号括起来的字符串的话也是同样处理--直接入栈。</p> <p>截止现在,恭喜你,一个虚拟机就完成了。</p> <p>让我们定义更多的操作,然后使用我们刚完成的虚拟机和<a href="https://en.wikipedia.org/wiki/P-code_machine">p-code 语言</a> 来写程序。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="c"># Allow to use &quot;print&quot; as a name for our own method:</span> <span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">print_function</span> <span class="c"># ...</span> <span class="k">def</span> <span class="nf">plus</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="k">def</span> <span class="nf">minus</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">last</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">-</span> <span class="n">last</span><span class="p">)</span> <span class="k">def</span> <span class="nf">mul</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="k">def</span> <span class="nf">div</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">last</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">/</span> <span class="n">last</span><span class="p">)</span> <span class="k">def</span> <span class="nf">print</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()))</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span> <span class="k">def</span> <span class="nf">println</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s">&quot;</span><span class="si">%s</span><span class="se">\n</span><span class="s">&quot;</span> <span class="o">%</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span> </code></pre></div> <p>让我们用我们的虚拟机写个与 <code>print((2+3)*4)</code> 等同效果的例子。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Machine([2, 3, &quot;+&quot;, 4, &quot;*&quot;, &quot;println&quot;]).run() </code></pre></div> <p>你可以试着运行它。</p> <p>现在引入一个新的操作 jump, 即 go-to 操作</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">jmp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">addr</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">addr</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> <span class="ow">and</span> <span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">addr</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">=</span> <span class="n">addr</span> <span class="k">else</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s">&quot;JMP address must be a valid integer.&quot;</span><span class="p">)</span> </code></pre></div> <p>它只改变指令指针的值。我们再看看分支跳转是怎么做的。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">if_stmt</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">false_clause</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">true_clause</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">test</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">true_clause</span> <span class="k">if</span> <span class="n">test</span> <span class="k">else</span> <span class="n">false_clause</span><span class="p">)</span> </code></pre></div> <p>这同样也是很直白的。如果你想要添加一个条件跳转,你只要简单的执行 <code>test-value true-value false-value IF JMP</code> 就可以了.(分支处理是很常见的操作,许多虚拟机都提供类似 <code>JNE</code> 这样的操作。<code>JNE</code> 是 <code>jump if not equal</code> 的缩写)。</p> <p>下面的程序要求使用者输入两个数字,然后打印出他们的和和乘积。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">Machine</span><span class="p">([</span> <span class="s">&#39;&quot;Enter a number: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;Enter another number: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">,</span> <span class="s">&quot;over&quot;</span><span class="p">,</span> <span class="s">&quot;over&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;Their sum is: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;+&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;Their product is: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span> <span class="p">])</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> </code></pre></div> <p><code>over</code>、<code>read</code> 和 <code>cast_int</code> 这三个操作是长这样滴:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">cast_int</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()))</span> <span class="k">def</span> <span class="nf">over</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">b</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">a</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="k">def</span> <span class="nf">read</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="nb">raw_input</span><span class="p">())</span> </code></pre></div> <p>以下这一段程序要求使用者输入一个数字,然后打印出这个数字是奇数还是偶数。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">Machine</span><span class="p">([</span> <span class="s">&#39;&quot;Enter a number: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;The number &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;dup&quot;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&#39;&quot; is &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="s">&quot;%&quot;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&quot;==&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;even.&quot;&#39;</span><span class="p">,</span> <span class="s">&#39;&quot;odd.&quot;&#39;</span><span class="p">,</span> <span class="s">&quot;if&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&quot;jmp&quot;</span> <span class="c"># loop forever!</span> <span class="p">])</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> </code></pre></div> <p>这里有个小练习给你去实现:增加 <code>call</code> 和 <code>return</code> 这两个操作码。<code>call</code> 操作码将会做如下事情 :将当前地址推入返回堆栈中,然后调用 <code>self.jmp()</code>。<code>return</code> 操作码将会做如下事情:返回堆栈弹栈,将弹栈出来元素的值赋予指令指针(这个值可以让你跳转回去或者从 <code>call</code> 调用中返回)。当你完成这两个命令,那么你的虚拟机就可以调用子例程了。</p> <h2>一个简单的解析器</h2> <p>创造一个模仿上述程序的小型语言。我们将把它编译成我们的机器码。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="nn">tokenize</span> <span class="kn">from</span> <span class="nn">StringIO</span> <span class="kn">import</span> <span class="n">StringIO</span> <span class="c"># ...</span> <span class="k">def</span> <span class="nf">parse</span><span class="p">(</span><span class="n">text</span><span class="p">):</span> <span class="n">tokens</span> <span class="o">=</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">generate_tokens</span><span class="p">(</span><span class="n">StringIO</span><span class="p">(</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">readline</span><span class="p">)</span> <span class="k">for</span> <span class="n">toknum</span><span class="p">,</span> <span class="n">tokval</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">tokens</span><span class="p">:</span> <span class="k">if</span> <span class="n">toknum</span> <span class="o">==</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">NUMBER</span><span class="p">:</span> <span class="k">yield</span> <span class="nb">int</span><span class="p">(</span><span class="n">tokval</span><span class="p">)</span> <span class="k">elif</span> <span class="n">toknum</span> <span class="ow">in</span> <span class="p">[</span><span class="n">tokenize</span><span class="o">.</span><span class="n">OP</span><span class="p">,</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">STRING</span><span class="p">,</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">NAME</span><span class="p">]:</span> <span class="k">yield</span> <span class="n">tokval</span> <span class="k">elif</span> <span class="n">toknum</span> <span class="o">==</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">ENDMARKER</span><span class="p">:</span> <span class="k">break</span> <span class="k">else</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s">&quot;Unknown token </span><span class="si">%s</span><span class="s">: &#39;</span><span class="si">%s</span><span class="s">&#39;&quot;</span> <span class="o">%</span> <span class="p">(</span><span class="n">tokenize</span><span class="o">.</span><span class="n">tok_name</span><span class="p">[</span><span class="n">toknum</span><span class="p">],</span> <span class="n">tokval</span><span class="p">))</span> </code></pre></div> <h2>一个简单的优化:常量折叠</h2> <p>常量折叠(<a href="https://en.wikipedia.org/wiki/Constant_folding">Constant folding</a>)是窥孔优化(<a href="https://en.wikipedia.org/wiki/Peephole_optimization">peephole optimization</a>)的一个例子,也即是说再在编译期间可以针对某些明显的代码片段做些预计算的工作。比如,对于涉及到常量的数学表达式例如 <code>2 3 +</code> 就可以很轻松的实现这种优化。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">constant_fold</span><span class="p">(</span><span class="n">code</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Constant-folds simple mathematical expressions like 2 3 + to 5.&quot;&quot;&quot;</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="c"># Find two consecutive numbers and an arithmetic operator</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">op</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">code</span><span class="p">[</span><span class="mi">1</span><span class="p">:],</span> <span class="n">code</span><span class="p">[</span><span class="mi">2</span><span class="p">:])):</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> \ <span class="ow">and</span> <span class="n">op</span> <span class="ow">in</span> <span class="p">{</span><span class="s">&quot;+&quot;</span><span class="p">,</span> <span class="s">&quot;-&quot;</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">,</span> <span class="s">&quot;/&quot;</span><span class="p">}:</span> <span class="n">m</span> <span class="o">=</span> <span class="n">Machine</span><span class="p">((</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">op</span><span class="p">))</span> <span class="n">m</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="n">code</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span><span class="o">+</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">m</span><span class="o">.</span><span class="n">top</span><span class="p">()]</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Constant-folded </span><span class="si">%s%s%s</span><span class="s"> to </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">op</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">m</span><span class="o">.</span><span class="n">top</span><span class="p">()))</span> <span class="k">break</span> <span class="k">else</span><span class="p">:</span> <span class="k">break</span> <span class="k">return</span> <span class="n">code</span> </code></pre></div> <p>采用常量折叠遇到唯一问题就是我们不得不更新跳转地址,但在很多情况这是很难办到的(例如:<code>test cast_int jmp</code>)。针对这个问题有很多解决方法,其中一个简单的方法就是只允许跳转到程序中的命名标签上,然后在优化之后解析出他们真正的地址。</p> <p>如果你实现了 Forth words,也即函数,你可以做更多的优化,比如删除可能永远不会被用到的程序代码(<a href="https://en.wikipedia.org/wiki/Dead_code_elimination">dead code elimination</a>)</p> <h2>REPL</h2> <p>我们可以创造一个简单的 PERL,就像这样</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">repl</span><span class="p">():</span> <span class="k">print</span><span class="p">(</span><span class="s">&#39;Hit CTRL+D or type &quot;exit&quot; to quit.&#39;</span><span class="p">)</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="n">source</span> <span class="o">=</span> <span class="nb">raw_input</span><span class="p">(</span><span class="s">&quot;&gt; &quot;</span><span class="p">)</span> <span class="n">code</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">parse</span><span class="p">(</span><span class="n">source</span><span class="p">))</span> <span class="n">code</span> <span class="o">=</span> <span class="n">constant_fold</span><span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="n">Machine</span><span class="p">(</span><span class="n">code</span><span class="p">)</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="k">except</span> <span class="p">(</span><span class="ne">RuntimeError</span><span class="p">,</span> <span class="ne">IndexError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;IndexError: </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="n">e</span><span class="p">)</span> <span class="k">except</span> <span class="ne">KeyboardInterrupt</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;</span><span class="se">\n</span><span class="s">KeyboardInterrupt&quot;</span><span class="p">)</span> </code></pre></div> <p>用一些简单的程序来测试我们的 REPL </p> <div class="highlight"><pre><code class="language-text" data-lang="text">&gt; 2 3 + 4 * println Constant-folded 2+3 to 5 Constant-folded 5*4 to 20 20 &gt; 12 dup * println 144 &gt; &quot;Hello, world!&quot; dup println println Hello, world! Hello, world! </code></pre></div> <p>你可以看到,常量折叠看起来运转正常。在第一个例子中,它把整个程序优化成这样 <code>20 println</code>。</p> <h2>下一步</h2> <p>当你添加完 <code>call</code> 和 <code>return</code> 之后,你便可以让使用者定义自己的函数了。在<a href="https://en.wikipedia.org/wiki/Forth_(programming_language)">Forth</a> 中函数被称为 words,他们以冒号开头紧接着是名字然后以分号结束。例如,一个整数平方的 word 是长这样滴</p> <div class="highlight"><pre><code class="language-text" data-lang="text">: square dup * ; </code></pre></div> <p>实际上,你可以试试把这一段放在程序中,比如 <a href="https://www.gnu.org/software/gforth/">Gforth</a></p> <div class="highlight"><pre><code class="language-text" data-lang="text">$ gforth Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc. Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license&#39; Type `bye&#39; to exit : square dup * ; ok 12 square . 144 ok </code></pre></div> <p>你可以在解析器中通过发现 <code>:</code> 来支持这一点。一旦你发现一个冒号,你必须记录下它的名字及其地址(比如:在程序中的位置)然后把他们插入到符号表(<a href="https://en.wikipedia.org/wiki/Symbol_table">symbol table</a>)中。简单起见,你甚至可以把整个函数的代码(包括分号)放在字典中,譬如:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">symbol_table = { &quot;square&quot;: [&quot;dup&quot;, &quot;*&quot;] # ... } </code></pre></div> <p>当你完成了解析的工作,你可以<a href="https://en.wikipedia.org/wiki/Linker_(computing)">连接</a>你的程序:遍历整个主程序并且在符号表中寻找自定义函数的地方。一旦你找到一个并且它没有在主程序的后面出现,那么你可以把它附加到主程序的后面。然后用 <code>&lt;address&gt; call</code> 替换掉 <code>square</code>,这里的 <code>&lt;address&gt;</code> 是函数插入的地址。</p> <p>为了保证程序能正常执行,你应该考虑剔除 <code>jmp</code> 操作。否则的话,你不得不解析它们。它确实能执行,但是你得按照用户编写程序的顺序保存它们。举例来说,你想在子例程之间移动,你要格外小心。你可能需要添加 <code>exit</code> 函数用于停止程序(可能需要告诉操作系统返回值),这样主程序就不会继续执行以至于跑到子例程中。</p> <p>实际上,一个好的程序空间布局很有可能把主程序当成一个名为 <code>main</code> 的子例程。或者由你决定搞成什么样子。</p> <p>如您所见,这一切都是很有趣的,而且通过这一过程你也学会了很多关于代码生成、链接、程序空间布局相关的知识。</p> <h2>更多能做的事儿</h2> <p>你可以使用 Python 字节码生成库来尝试将虚拟机代码为原生的 Python 字节码。或者用 Java 实现运行在 JVM 上面,这样你就可以自由使用 <a href="https://en.wikipedia.org/wiki/Just-in-time_compilation">JITing</a>。</p> <p>同样的,你也可以尝试下<a href="https://en.wikipedia.org/wiki/Register_machine">register machine</a>。你可以尝试用栈帧(<a href="https://en.wikipedia.org/wiki/Call_stack#STACK-FRAME">stack frames</a>)实现调用栈(<a href="https://en.wikipedia.org/wiki/Call_stack">call stack</a>),并基于此建立调用会话。</p> <p>最后,如果你不喜欢类似 Forth 这样的语言,你可以创造运行于这个虚拟机之上的自定义语言。譬如,你可以把类似 <code>(2+3)*4</code> 这样的中缀表达式转化成 <code>2 3 + 4 *</code> 然后生成代码。你也可以允许 C 风格的代码块 <code>{ ... }</code> 这样的话,语句 <code>if ( test ) { ... } else { ... }</code> 将会被翻译成</p> <div class="highlight"><pre><code class="language-text" data-lang="text">&lt;true/false test&gt; &lt;address of true block&gt; &lt;address of false block&gt; if jmp &lt;true block&gt; &lt;address of end of entire if-statement&gt; jmp &lt;false block&gt; &lt;address of end of entire if-statement&gt; jmp </code></pre></div> <p>例子,</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Address Code ------- ---- 0 2 3 &gt; 3 7 # Address of true-block 4 11 # Address of false-block 5 if 6 jmp # Conditional jump based on test # True-block 7 &quot;Two is greater than three.&quot; 8 println 9 15 # Continue main program 10 jmp # False-block (&quot;else { ... }&quot;) 11 &quot;Two is less than three.&quot; 12 println 13 15 # Continue main program 14 jmp # If-statement finished, main program continues here 15 ... </code></pre></div> <p>对了,你还需要添加比较操作符 <code>!= &lt; &lt;= &gt; &gt;=</code>。</p> <p>我已经在我的 <a href="https://github.com/cslarsen/stack-machine">C++ stack machine</a> 实现了这些东东,你可以参考下。</p> <p>我已经把这里呈现出来的代码搞成了个项目 <a href="https://github.com/cslarsen/crianza">Crianza</a>,它使用了更多的优化和实验性质的模型来吧程序编译成 Python 字节码。</p> <p>祝好运!</p> <h2>完整的代码</h2> <p>下面是全部的代码,兼容 Python 2 和 Python 3</p> <p>你可以通过<a href="https://github.com/cslarsen/python-simple-vm">这里</a> 得到它。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="c">#!/usr/bin/env python</span> <span class="c"># coding: utf-8</span> <span class="sd">&quot;&quot;&quot;</span> <span class="sd">A simple VM interpreter.</span> <span class="sd">Code from the post at http://csl.name/post/vm/</span> <span class="sd">This version should work on both Python 2 and 3.</span> <span class="sd">&quot;&quot;&quot;</span> <span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">print_function</span> <span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">deque</span> <span class="kn">from</span> <span class="nn">io</span> <span class="kn">import</span> <span class="n">StringIO</span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">tokenize</span> <span class="k">def</span> <span class="nf">get_input</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Read a string from standard input.&quot;&quot;&quot;</span> <span class="k">if</span> <span class="n">sys</span><span class="o">.</span><span class="n">version</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s">&quot;2&quot;</span><span class="p">:</span> <span class="k">return</span> <span class="nb">raw_input</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="k">return</span> <span class="nb">input</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">)</span> <span class="k">class</span> <span class="nc">Stack</span><span class="p">(</span><span class="n">deque</span><span class="p">):</span> <span class="n">push</span> <span class="o">=</span> <span class="n">deque</span><span class="o">.</span><span class="n">append</span> <span class="k">def</span> <span class="nf">top</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="k">class</span> <span class="nc">Machine</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">code</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span> <span class="o">=</span> <span class="n">Stack</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">return_stack</span> <span class="o">=</span> <span class="n">Stack</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">=</span> <span class="mi">0</span> <span class="bp">self</span><span class="o">.</span><span class="n">code</span> <span class="o">=</span> <span class="n">code</span> <span class="k">def</span> <span class="nf">pop</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="k">def</span> <span class="nf">push</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">def</span> <span class="nf">top</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="o">.</span><span class="n">top</span><span class="p">()</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">while</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">):</span> <span class="n">opcode</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span><span class="p">]</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">+=</span> <span class="mi">1</span> <span class="bp">self</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">opcode</span><span class="p">)</span> <span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">op</span><span class="p">):</span> <span class="n">dispatch_map</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&quot;%&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mod</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mul</span><span class="p">,</span> <span class="s">&quot;+&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">plus</span><span class="p">,</span> <span class="s">&quot;-&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">minus</span><span class="p">,</span> <span class="s">&quot;/&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">div</span><span class="p">,</span> <span class="s">&quot;==&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eq</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cast_int</span><span class="p">,</span> <span class="s">&quot;cast_str&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cast_str</span><span class="p">,</span> <span class="s">&quot;drop&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">drop</span><span class="p">,</span> <span class="s">&quot;dup&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">dup</span><span class="p">,</span> <span class="s">&quot;exit&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">exit</span><span class="p">,</span> <span class="s">&quot;if&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">if_stmt</span><span class="p">,</span> <span class="s">&quot;jmp&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">jmp</span><span class="p">,</span> <span class="s">&quot;over&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">over</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="k">print</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">println</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">read</span><span class="p">,</span> <span class="s">&quot;stack&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">dump_stack</span><span class="p">,</span> <span class="s">&quot;swap&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">swap</span><span class="p">,</span> <span class="p">}</span> <span class="k">if</span> <span class="n">op</span> <span class="ow">in</span> <span class="n">dispatch_map</span><span class="p">:</span> <span class="n">dispatch_map</span><span class="p">[</span><span class="n">op</span><span class="p">]()</span> <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="nb">int</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">op</span><span class="p">)</span> <span class="c"># push numbers on stack</span> <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="ow">and</span> <span class="n">op</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">==</span><span class="n">op</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">==</span><span class="s">&#39;&quot;&#39;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">op</span><span class="p">[</span><span class="mi">1</span><span class="p">:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="c"># push quoted strings on stack</span> <span class="k">else</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s">&quot;Unknown opcode: &#39;</span><span class="si">%s</span><span class="s">&#39;&quot;</span> <span class="o">%</span> <span class="n">op</span><span class="p">)</span> <span class="c"># OPERATIONS FOLLOW:</span> <span class="k">def</span> <span class="nf">plus</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="k">def</span> <span class="nf">exit</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="k">def</span> <span class="nf">minus</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">last</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">-</span> <span class="n">last</span><span class="p">)</span> <span class="k">def</span> <span class="nf">mul</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="k">def</span> <span class="nf">div</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">last</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">/</span> <span class="n">last</span><span class="p">)</span> <span class="k">def</span> <span class="nf">mod</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">last</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">%</span> <span class="n">last</span><span class="p">)</span> <span class="k">def</span> <span class="nf">dup</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">top</span><span class="p">())</span> <span class="k">def</span> <span class="nf">over</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">b</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">a</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="k">def</span> <span class="nf">drop</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="k">def</span> <span class="nf">swap</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">b</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">a</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="k">def</span> <span class="nf">print</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()))</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span> <span class="k">def</span> <span class="nf">println</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s">&quot;</span><span class="si">%s</span><span class="se">\n</span><span class="s">&quot;</span> <span class="o">%</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span> <span class="k">def</span> <span class="nf">read</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">get_input</span><span class="p">())</span> <span class="k">def</span> <span class="nf">cast_int</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()))</span> <span class="k">def</span> <span class="nf">cast_str</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()))</span> <span class="k">def</span> <span class="nf">eq</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">())</span> <span class="k">def</span> <span class="nf">if_stmt</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">false_clause</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">true_clause</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="n">test</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">true_clause</span> <span class="k">if</span> <span class="n">test</span> <span class="k">else</span> <span class="n">false_clause</span><span class="p">)</span> <span class="k">def</span> <span class="nf">jmp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">addr</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">addr</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> <span class="ow">and</span> <span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">addr</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">instruction_pointer</span> <span class="o">=</span> <span class="n">addr</span> <span class="k">else</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s">&quot;JMP address must be a valid integer.&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">dump_stack</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Data stack (top first):&quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">data_stack</span><span class="p">):</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot; - type </span><span class="si">%s</span><span class="s">, value &#39;</span><span class="si">%s</span><span class="s">&#39;&quot;</span> <span class="o">%</span> <span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">v</span><span class="p">),</span> <span class="n">v</span><span class="p">))</span> <span class="k">def</span> <span class="nf">parse</span><span class="p">(</span><span class="n">text</span><span class="p">):</span> <span class="c"># Note that the tokenizer module is intended for parsing Python source</span> <span class="c"># code, so if you&#39;re going to expand on the parser, you may have to use</span> <span class="c"># another tokenizer.</span> <span class="k">if</span> <span class="n">sys</span><span class="o">.</span><span class="n">version</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s">&quot;2&quot;</span><span class="p">:</span> <span class="n">stream</span> <span class="o">=</span> <span class="n">StringIO</span><span class="p">(</span><span class="nb">unicode</span><span class="p">(</span><span class="n">text</span><span class="p">))</span> <span class="k">else</span><span class="p">:</span> <span class="n">stream</span> <span class="o">=</span> <span class="n">StringIO</span><span class="p">(</span><span class="n">text</span><span class="p">)</span> <span class="n">tokens</span> <span class="o">=</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">generate_tokens</span><span class="p">(</span><span class="n">stream</span><span class="o">.</span><span class="n">readline</span><span class="p">)</span> <span class="k">for</span> <span class="n">toknum</span><span class="p">,</span> <span class="n">tokval</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">tokens</span><span class="p">:</span> <span class="k">if</span> <span class="n">toknum</span> <span class="o">==</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">NUMBER</span><span class="p">:</span> <span class="k">yield</span> <span class="nb">int</span><span class="p">(</span><span class="n">tokval</span><span class="p">)</span> <span class="k">elif</span> <span class="n">toknum</span> <span class="ow">in</span> <span class="p">[</span><span class="n">tokenize</span><span class="o">.</span><span class="n">OP</span><span class="p">,</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">STRING</span><span class="p">,</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">NAME</span><span class="p">]:</span> <span class="k">yield</span> <span class="n">tokval</span> <span class="k">elif</span> <span class="n">toknum</span> <span class="o">==</span> <span class="n">tokenize</span><span class="o">.</span><span class="n">ENDMARKER</span><span class="p">:</span> <span class="k">break</span> <span class="k">else</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s">&quot;Unknown token </span><span class="si">%s</span><span class="s">: &#39;</span><span class="si">%s</span><span class="s">&#39;&quot;</span> <span class="o">%</span> <span class="p">(</span><span class="n">tokenize</span><span class="o">.</span><span class="n">tok_name</span><span class="p">[</span><span class="n">toknum</span><span class="p">],</span> <span class="n">tokval</span><span class="p">))</span> <span class="k">def</span> <span class="nf">constant_fold</span><span class="p">(</span><span class="n">code</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Constant-folds simple mathematical expressions like 2 3 + to 5.&quot;&quot;&quot;</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="c"># Find two consecutive numbers and an arithmetic operator</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">op</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">code</span><span class="p">[</span><span class="mi">1</span><span class="p">:],</span> <span class="n">code</span><span class="p">[</span><span class="mi">2</span><span class="p">:])):</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> \ <span class="ow">and</span> <span class="n">op</span> <span class="ow">in</span> <span class="p">{</span><span class="s">&quot;+&quot;</span><span class="p">,</span> <span class="s">&quot;-&quot;</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">,</span> <span class="s">&quot;/&quot;</span><span class="p">}:</span> <span class="n">m</span> <span class="o">=</span> <span class="n">Machine</span><span class="p">((</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">op</span><span class="p">))</span> <span class="n">m</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="n">code</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span><span class="o">+</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">m</span><span class="o">.</span><span class="n">top</span><span class="p">()]</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Constant-folded </span><span class="si">%s%s%s</span><span class="s"> to </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">op</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">m</span><span class="o">.</span><span class="n">top</span><span class="p">()))</span> <span class="k">break</span> <span class="k">else</span><span class="p">:</span> <span class="k">break</span> <span class="k">return</span> <span class="n">code</span> <span class="k">def</span> <span class="nf">repl</span><span class="p">():</span> <span class="k">print</span><span class="p">(</span><span class="s">&#39;Hit CTRL+D or type &quot;exit&quot; to quit.&#39;</span><span class="p">)</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="n">source</span> <span class="o">=</span> <span class="n">get_input</span><span class="p">(</span><span class="s">&quot;&gt; &quot;</span><span class="p">)</span> <span class="n">code</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">parse</span><span class="p">(</span><span class="n">source</span><span class="p">))</span> <span class="n">code</span> <span class="o">=</span> <span class="n">constant_fold</span><span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="n">Machine</span><span class="p">(</span><span class="n">code</span><span class="p">)</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="k">except</span> <span class="p">(</span><span class="ne">RuntimeError</span><span class="p">,</span> <span class="ne">IndexError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;IndexError: </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="n">e</span><span class="p">)</span> <span class="k">except</span> <span class="ne">KeyboardInterrupt</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;</span><span class="se">\n</span><span class="s">KeyboardInterrupt&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">test</span><span class="p">(</span><span class="n">code</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s">&quot;+&quot;</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">]):</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Code before optimization: </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="nb">str</span><span class="p">(</span><span class="n">code</span><span class="p">))</span> <span class="n">optimized</span> <span class="o">=</span> <span class="n">constant_fold</span><span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Code after optimization: </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="nb">str</span><span class="p">(</span><span class="n">optimized</span><span class="p">))</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Stack after running original program:&quot;</span><span class="p">)</span> <span class="n">a</span> <span class="o">=</span> <span class="n">Machine</span><span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="n">a</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="n">a</span><span class="o">.</span><span class="n">dump_stack</span><span class="p">()</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Stack after running optimized program:&quot;</span><span class="p">)</span> <span class="n">b</span> <span class="o">=</span> <span class="n">Machine</span><span class="p">(</span><span class="n">optimized</span><span class="p">)</span> <span class="n">b</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="n">b</span><span class="o">.</span><span class="n">dump_stack</span><span class="p">()</span> <span class="n">result</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="n">data_stack</span> <span class="o">==</span> <span class="n">b</span><span class="o">.</span><span class="n">data_stack</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Result: </span><span class="si">%s</span><span class="s">&quot;</span> <span class="o">%</span> <span class="p">(</span><span class="s">&quot;OK&quot;</span> <span class="k">if</span> <span class="n">result</span> <span class="k">else</span> <span class="s">&quot;FAIL&quot;</span><span class="p">))</span> <span class="k">return</span> <span class="n">result</span> <span class="k">def</span> <span class="nf">examples</span><span class="p">():</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;** Program 1: Runs the code for `print((2+3)*4)`&quot;</span><span class="p">)</span> <span class="n">Machine</span><span class="p">([</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s">&quot;+&quot;</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">])</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;</span><span class="se">\n</span><span class="s">** Program 2: Ask for numbers, computes sum and product.&quot;</span><span class="p">)</span> <span class="n">Machine</span><span class="p">([</span> <span class="s">&#39;&quot;Enter a number: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;Enter another number: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">,</span> <span class="s">&quot;over&quot;</span><span class="p">,</span> <span class="s">&quot;over&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;Their sum is: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;+&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;Their product is: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;*&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span> <span class="p">])</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;</span><span class="se">\n</span><span class="s">** Program 3: Shows branching and looping (use CTRL+D to exit).&quot;</span><span class="p">)</span> <span class="n">Machine</span><span class="p">([</span> <span class="s">&#39;&quot;Enter a number: &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;read&quot;</span><span class="p">,</span> <span class="s">&quot;cast_int&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;The number &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&quot;dup&quot;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="s">&#39;&quot; is &quot;&#39;</span><span class="p">,</span> <span class="s">&quot;print&quot;</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="s">&quot;%&quot;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&quot;==&quot;</span><span class="p">,</span> <span class="s">&#39;&quot;even.&quot;&#39;</span><span class="p">,</span> <span class="s">&#39;&quot;odd.&quot;&#39;</span><span class="p">,</span> <span class="s">&quot;if&quot;</span><span class="p">,</span> <span class="s">&quot;println&quot;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&quot;jmp&quot;</span> <span class="c"># loop forever!</span> <span class="p">])</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span> <span class="n">cmd</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">if</span> <span class="n">cmd</span> <span class="o">==</span> <span class="s">&quot;repl&quot;</span><span class="p">:</span> <span class="n">repl</span><span class="p">()</span> <span class="k">elif</span> <span class="n">cmd</span> <span class="o">==</span> <span class="s">&quot;test&quot;</span><span class="p">:</span> <span class="n">test</span><span class="p">()</span> <span class="n">examples</span><span class="p">()</span> <span class="k">else</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Commands: repl, test&quot;</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="n">repl</span><span class="p">()</span> <span class="k">except</span> <span class="ne">EOFError</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;&quot;</span><span class="p">)</span> </code></pre></div> 有关网页渲染,每个前端开发者都该知道的那点事! /javascript/2015/06/16/reflow/ Tue, 16 Jun 2015 00:00:00 +0000 /javascript/2015/06/16/reflow <p>【编者按】其实,有关网页渲染的文章很多,但是相关信息比较分散,且论述并不是很完整。如果要想对这个主题有个大致的了解,我们还得学习很多知识。因此,Web开发者Alexander Skutin 决定写一篇文章。他相信,这篇文章不仅能帮助初学者,也能对那些想要刷新知识结构的高级前端开发者有所裨益。<a href="http://frontendbabel.info/articles/webpage-rendering-101/">原文地址</a></p> <p><strong>译文如下:</strong></p> <p>网页渲染必须在很早的阶段进行,可以早到页面布局刚刚定型。因为样式和脚本都会对网页渲染产生关键性的影响。所以专业开发者必须了解一些技巧,从而避免在实践的过程中遇到性能问题。</p> <p>这篇文章不会研究浏览器内部的详细机制,而是提出一些通用的规则。毕竟,不同浏览器引擎的工作机制各不相同,这无疑会让开发者对浏览器特性的研究变得更加复杂。</p> <h2>浏览器是如何完成网页渲染?</h2> <p>首先,我们回顾一下网页渲染时,浏览器的动作:</p> <ol> <li>根据来自服务器端的HTML代码形成文档对象模型(DOM)</li> <li>加载并解析样式,形成CSS对象模型。</li> <li>在文档对象模型和CSS对象模型之上,创建一棵由一组待生成渲染的对象组成的渲染树(在Webkit中这些对象被称为渲染器或渲染对象,而在Gecko中称之为“frame”。)渲染树反映了文档对象模型的结构,但是不包含诸如<head>标签或含有<code>display:none</code>属性的不可见元素。在渲染树中,每一段文本字符串都表现为独立的渲染器。每一个渲染对象都包含与之对应的DOM对象,或者文本块,还加上计算过的样式。换言之,渲染树是一个文档对象模型的直观展示。</li> <li><p>对渲染树上的每个元素,计算它的坐标,称之为布局。浏览器采用一种流方法,布局一个元素只需通过一次,但是表格元素需要通过多次。</p></li> <li><p>最后,渲染树上的元素最终展示在浏览器里,这一过程称为“painting”。</p></li> </ol> <p>当用户与网页交互,或者脚本程序改动修改网页时,前文提到的一些操作将会重复执行,因为网页的内在结构已经发生了改变。</p> <h2>重绘</h2> <p>当改变那些不会影响元素在网页中的位置的元素样式时,譬如background-color(背景色), border-color(边框色), visibility(可见性),浏览器只会用新的样式将元素重绘一次(这就是重绘,或者说重新构造样式)。</p> <h2>重排</h2> <p>当改变影响到文本内容或结构,或者元素位置时,重排或者说重新布局就会发生。这些改变通常由以下事件触发:</p> <ul> <li>DOM操作(元素添加,删除,修改,或者元素顺序的改变);</li> <li>内容变化,包括表单域内的文本改变;</li> <li>CSS属性的计算或改变;</li> <li>添加或删除样式表;</li> <li>更改“类”的属性;</li> <li>浏览器窗口的操作(缩放,滚动);</li> <li>伪类激活(:悬停)。</li> </ul> <h2>浏览器如何优化渲染?</h2> <p>浏览器尽可能将重绘/重构 限制在被改变元素的区域内。比如,对于位置固定或绝对的元素,其大小改变只影响元素本身及其子元素,然而,静态定位元素的大小改变会触发后续所有元素的重流。</p> <p>另一种优化技巧是,在运行几段JavaScript代码时,浏览器会缓存这些改变,在代码运行完毕后再将这些改变经一次通过加以应用。举个例子,下面这段代码只会触发一个重构和重绘: <code></p> <div class="highlight"><pre><code class="language-text" data-lang="text">var $body = $(&#39;body&#39;); $body.css(&#39;padding&#39;, &#39;1px&#39;); // reflow, repaint $body.css(&#39;color&#39;, &#39;red&#39;); // repaint $body.css(&#39;margin&#39;, &#39;2px&#39;); // reflow, repaint // only 1 reflow and repaint will actually happen </code></pre></div> <p></code></p> <p>然而,如前所述,改变元素的属性会触发强制性的重排。如果我们在上面的代码块中加入一行代码,用来访问元素的属性,就会发生这种现象。</p> <p><code> var $body = $(&#39;body&#39;); $body.css(&#39;padding&#39;, &#39;1px&#39;); $body.css(&#39;padding&#39;); // reading a property, a forced reflow $body.css(&#39;color&#39;, &#39;red&#39;); $body.css(&#39;margin&#39;, &#39;2px&#39;); </code></p> <p>其结果就是,重排发生了两次。因此,你应该把访问元素属性的操作都组织在一起,从而优化网页性能。(<a href="http://jsbin.com/duhah/2/edit?html,css,js,output">你可以在JSBin查到更为详细的例子</a>)</p> <p>有时,你必须触发一个强制性重排。比如,我们必须将同样的属性(比如左边距)两次赋值给同一个元素。起初,它应该设置为100px,且不带动效。接着,它必须通过过渡(transition)动效改变为50px。你现在可以在<a href="http://jsbin.com/duhah/2/edit?html,css,js,output">JSbin</a>上学习这个例子,不过我会在这儿更详细地介绍它。</p> <p>首先,我们创建一个带过渡效果的CSS类: <code> .has-transition { -webkit-transition: margin-left 1s ease-out; -moz-transition: margin-left 1s ease-out; -o-transition: margin-left 1s ease-out; transition: margin-left 1s ease-out; } </code></p> <p>然后继续执行: <code> // our element that has a &quot;has-transition&quot; class by default var $targetElem = $(&#39;#targetElemId&#39;);</p> <div class="highlight"><pre><code class="language-text" data-lang="text">// remove the transition class $targetElem.removeClass(&#39;has-transition&#39;); // change the property expecting the transition to be off, as the class is not there // anymore $targetElem.css(&#39;margin-left&#39;, 100); // put the transition class back $targetElem.addClass(&#39;has-transition&#39;); // change the property $targetElem.css(&#39;margin-left&#39;, 50); </code></pre></div> <p></code> 然而,这个执行无法奏效。所有改变都被缓存,只在代码块末尾加以执行。我们需要的是强制性的重排,我们可以通过以下更改加以实现:</p> <p><code> // remove the transition class $(this).removeClass(&#39;has-transition&#39;);</p> <div class="highlight"><pre><code class="language-text" data-lang="text">// change the property $(this).css(&#39;margin-left&#39;, 100); // trigger a forced reflow, so that changes in a class/property get applied immediately $(this)[0].offsetHeight; // an example, other properties would work, too // put the transition class back $(this).addClass(&#39;has-transition&#39;); // change the property $(this).css(&#39;margin-left&#39;, 50); </code></pre></div> <p></code> 现在代码如预期那样执行了。</p> <h2>有关性能优化的实际建议</h2> <p>总结现有的资料,我提出以下建议:</p> <ul> <li>创建有效的HTML和CSS文件,不要忘记指明文档的编码方式。样式应该包含在<head>标签内,脚本代码则应该加在<body>标签末端。</li> <li>尽量简化和优化CSS选择器(这种优化方式几乎被使用CSS预处理器的开发者统一忽视了)将嵌套程度保持在最低水平。以下是CSS选择器的性能排名(从最快者开始)</li> </ul> <div class="highlight"><pre><code class="language-text" data-lang="text">1. 识别器:#id 2. 类:.class 3. 标签:div 4. 相邻兄弟选择器:a + i 5. 父类选择器:ul&gt; li 6. 通用选择器:* 7. 属性选择:input[type=&quot;text&quot;] 8. 伪类和伪元素:a:hover </code></pre></div> <p>你应该记住,浏览器在处理选择器时依照从右到左的原则,因此最右端的选择器应该是最快的:#id或则.class: <code> div * {...} // bad .list li {...} // bad .list-item {...} // good #list .list-item {...} // good </code> * 1.在你的脚本代码中,尽可能减少DOM操作。缓存所有东西,包括元素属性以及对象(如果它们被重用的话)。当进行复杂的操作时,使用“孤立”元素会更好,之后可以将其加到DOM中(所谓“孤立”元素是与DOM脱离,仅保存在内存中的元素)。</p> <ul> <li><p>2.如果你使用jQuery来选择元素,请遵从jQuery选择器最佳实践方案。</p></li> <li><p>3.为了改变元素的样式,修改“类”的属性是奏效的方法之一。执行这一改变时,处在DOM渲染树的位置越深越好(这还有助于将逻辑与表象脱离)。</p></li> <li><p>4.尽量只给位置绝对或者固定的元素添加动画效果。</p></li> <li><p>5.在使用滚动时禁用复杂的悬停动效(比如,在<body>中添加一个额外的不悬停类)。读者可以阅读关于这个问题的<a href="http://habrahabr.ru/post/204238/">一篇文章</a>。</p></li> </ul> <p>想了解更多的细节问题,大家也可以看看这两篇文章:</p> <ul> <li>1,<a href="http://taligarsiel.com/Projects/howbrowserswork1.htm">How browsers work?</a></li> <li>2,<a href="http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/">Rendering: repaint, reflow/relayout, restyle</a></li> </ul> 企业级 Java 应用最重要的4个性能指标 /pm/2015/06/16/Java-enterprise/ Tue, 16 Jun 2015 00:00:00 +0000 /pm/2015/06/16/Java-enterprise <h1>企业级Java应用最重要的4个性能指标</h1> <p>应用性能管理(APM)是一种即时监控以实现对应用程序性能管理和故障管理的系统化解决方案。目前主要指对企业的关键业务应用进行监测、优化,最终达到提高企业应用的可靠性和质量,保证用户得到良好的服务,降低IT成本的目标。</p> <p>虽然很多人都曾预言Java将一蹶不振,但是不可否认的是,很多重要项目中,尤其是银行和政府一些大型项目,Java仍在其中扮演着极其重要的角色。国内APM领军企业OneAPM的Java工程师陶炳哲,多次参与银行、运营商等大型企业的性能优化工作,总结了企业级Java应用最应重视的4个性能指标,主要包括:商业事务,外部服务,垃圾回收以及应用布局。下文将逐一展开阐述:</p> <h2>1.商业事务</h2> <p>商业事务是真实用户体验的直观反映:它们抓取了用户与应用交互时,用户体验到的实时性能数据。测量商业事务的性能,需要抓取一件商业事务整体的响应时间及其各个组件的响应时间。这些响应时间再与满足业务需求的基准进行比较,从而决定应用是否正常。</p> <p><img src="http://i.imgur.com/0bHKjRD.png" alt="图一:OneAPM支持用户自定义自己的商业事务"></p> <p>如果你只打算测量应用的一个方面,本文会推荐你测量商业事务的表现。尽管容量指标(container metrics)能帮助你决定何时调节集群规模,但是商业事务才决定了应用本身的性能。你无需询问应用服务器线程池(thread pool)的使用情况,而是关心用户能否迅速完成他们的商业事务,以及这些事务的表现是否正常。</p> <p>介绍一点背景知识:商业事务通过其入口进行辨别,即用户与你的业务进行互动的入口。这类互动包括:一个网页请求,一个网页服务调用,或消息队列中的一条消息。当然,你也可以基于一个URL参数为同样的网页请求定义多个入口,或基于一个服务调用的内容定义多个入口点。关键在于:商业交易必须与对你的业务流程相关联,比如说中国移动的空中缴费业务对应到系统中是多个原子服务,我们就应该将这几个原子服务通过相应的关联聚合成一个空中缴费业务来进行监控。</p> <p>辨别某个商业交易后,它的性能就会在整个应用生态系统中进行测量。每个商业交易的性能会与其基准进行比较,判定其是否正常。譬如,如果某个商业事务的响应时间大于您设定的阈值,我们便判定其运行异常。<br> 总而言之,商业事务最能反映用户体验,因此它们也是最重要的抓取维度。</p> <h2>2.外部服务</h2> <p>外部服务的形式多种多样:从属的网页服务、遗留系统或数据库等。外部服务是与应用交互的系统。运行在外部服务系统中的代码常常无法控制,但是我们可以控制这些系统的配置,因此了解他们是否运行正常以及何时出错也很重要。并且,我们必须有能力区分问题是出自自身应用,还是源于这些外部服务系统。</p> <p><img src="http://i.imgur.com/TeSURkM.png" alt="图二: 系统往往会跟很多外部系统进行交互"></p> <p>从商业事务的角度来说,我们可以辨别并测量这些处于自身应用的外部服务。有时,我们需要配置监控方法从而辨别那些包裹了外部服务调用的方法。但是对于常见的协议,诸如HTTP和JDBC,外部服务可以自动检测。</p> <p>商业事务让你对应用的性能有了全局的掌控,帮助你对性能问题进行分类。但是外部服务总能以意想不到的方式极大地影响应用的运行,所以你必须监控它们。</p> <h2>3.垃圾回收</h2> <p>从Java发布最早版本开始,一直都保留的核心特性就是垃圾回收,它真是让人又爱又恨。垃圾回收使我们不再需要手动管理内存:当使用完一个对象后,我们只需删除它的引用,然后垃圾回收就会自动释放它。如果你使用过需要手动管理内存的语言,诸如C或C++,你会满怀感激。垃圾回收为程序员们减少了分配、释放内存空间的繁琐步骤。</p> <p><img src="http://i.imgur.com/F2AegAl.png" alt="图三: OneAPM提供对于JVM本身各项指标的监控"></p> <p>此外,因为垃圾回收器会自动释放没有引用的内存空间,它减少了传统的内容泄露情况,即内存被分配后,该内存的引用在内存释放前就被删除了。听起来就像灵丹妙药,不是么?</p> <p>尽管垃圾回收达成了无需手动管理内存的目标,也防止了传统的内存泄露,但是作为代价,垃圾回收过程有时相当笨拙。根据不同的JVM,垃圾回收策略也会不同。深入探讨这些策略超出了本文的主旨。但是,读者应该明白,了解垃圾回收期的工作原理,以及最佳的配置方案至关重要。</p> <p>垃圾回收最大的敌人就是传说中的主要(major)或(full)垃圾回收。除了Azul JVM,所有的JVM都有这个问题。通常,垃圾回收大致分为两类:</p> <ul> <li>次级</li> <li>主要</li> </ul> <p>为了释放存活时间较短的对象,次级垃圾回收发生得相对频繁。他们在运行时不会封锁线程,产生的影响较小。</p> <p>然而,主要垃圾回收,有时也称为“暂停世界(Stop The World, STW)”垃圾回收,因为他们在运行时会封锁JVM中的所有线程。</p> <p><img src="http://i.imgur.com/gKvwOV2.png" alt="图四: 当垃圾回收运行时,它会运行一项可达性测试"></p> <p>当垃圾回收运行时,它会运行一项可达性测试(reachability test),如图四所示。它会创建一个由对象组成的根集合(root set),该集合包含每个运行线程中的直接可见对象。接着,它会探寻根集合中的对象涉及的其他对象,然后探寻这些对象涉及的对象,直到所有对象都被涉及。在这个过程中,它会记录(mark)下现时活动对象的内存地址,然后把不被使用的所有地址都扫除(sweep)。说得更恰当些,它会把没有根集合对象引用的内存都释放。最终,它会压缩、整理这些内存,这样新的对象才能获得内存分配。</p> <p><img src="http://i.imgur.com/fEHPxHF.png" alt="图五: 次级回收"></p> <p>根据不同的JVM,次级、主要回收的方式都会不同。图五图六展示了在Sun JVM内次级、主要回收的操作方式。</p> <p>在次级回收中,内存主要分配到Eden空间直到将其填满。接着,拷贝收集器(copy collector)会将Eden中的活动对象拷贝到两个幸存者空间(survivor spaces, to space和from space)。遗留在Eden中的对象就会被移除。如果幸存者空间被填满,但还有多余的活动对象,这些对象会被移到tenured空间。只有主要回收才能释放tenured空间的内存。</p> <p>最终,tenured空间会被填满,主要回收将会执行。它不会将幸存者空间放不下的活动对象拷贝到tenured空间中。此时,JVM会封锁所有线程,运行可达性测试,清除年轻的数据(Eden和两个幸存者空间),并压缩tenured空间。我们将之称为主要回收。</p> <p><img src="http://i.imgur.com/iumG8tE.png" alt="图6:主要回收"></p> <p>你或许会想,堆越大,主要回收运行得越不频繁。但是当它执行时,所需时间就会比小堆要长。因此,调整好堆的大小和垃圾回收策略对于应用的性能也很重要。</p> <h2>4.应用布局</h2> <p>最后要探讨的性能指标是应用布局。因为云的出现,现在的应用变得更加灵活:应用环境可以根据用户需求调节大小。因此,对应用的布局进行检测从而决定实例的多少是否合适是非常重要的。如果你的实例太多,你的云主机成本就会增加。但如果你没有足够的实例,商业事务就会受到影响。</p> <p>在评测过程中,下面两个指标尤其重要:</p> <ul> <li><p>商业事务的吞吐量</p></li> <li><p>容器性能</p></li> </ul> <p>商业事务应该基准化,你应该知道在给定的时间里为了满足基准所需的实例数量。如果你的商业事务的吞吐量增长突然,你就要增加实例以满足用户。</p> <p>另一个需要监测的是容器性能。具体来说,你想确定是否有应用中的实例负载过大,如果有,你或许想在那个应用中添加实例。从应用的角度查看实例状态很重要,因为单个实例可能由于垃圾回收之类的因素负载过大,但如果应用中大多数实例都负载过大,则该应用可能已经无法支持它接受的访问量。</p> <p>因为应用中的实例可以单个地调节规模,所以分析各个实例的性能进而调整应用布局就至关重要。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】Python中如何创建mock? /python/2015/06/11/python-mock-introduction/ Thu, 11 Jun 2015 00:00:00 +0000 /python/2015/06/11/python-mock-introduction <p>原文地址:<a href="http://engineroom.trackmaven.com/blog/making-a-mockery-of-python/">http://engineroom.trackmaven.com/blog/making-a-mockery-of-python/</a></p> <p>今天我们来谈论下mock的使用。当然,请不要误会,这里的mock可不是嘲弄的意思。mock是一门技术,通过伪造部分实际代码,从而让我们能够验证剩余代码的正确性。现在我们将通过几个简单的示例演示mock在Python测试代码中的使用,以及这项极其有用的技术是如何帮助我们改善测试代码的。</p> <h2>为什么我们需要mock?</h2> <p>当我们进行单元测试的时候,我们的目标往往是为了测试非常小的代码块,例如一个独立存在的函数或类方法。换句话说,我们只需要针对那个函数内部的代码进行测试。如果测试代码依赖于其他的代码片段,即使被测试的函数没有变化,我们会发现在某种不幸的情形下,这部分内嵌代码的修改可能会破坏原有的测试。看看下面的例子,你将豁然开朗:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="c"># function.py</span> <span class="k">def</span> <span class="nf">add_and_multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span> <span class="n">addition</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span> <span class="n">multiple</span> <span class="o">=</span> <span class="n">multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="k">return</span> <span class="p">(</span><span class="n">addition</span><span class="p">,</span> <span class="n">multiple</span><span class="p">)</span> <span class="k">def</span> <span class="nf">multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span> <span class="k">return</span> <span class="n">x</span> <span class="o">*</span> <span class="n">y</span> <span class="c"># test.py</span> <span class="kn">import</span> <span class="nn">unittest</span> <span class="kn">from</span> <span class="nn">function</span> <span class="kn">import</span> <span class="n">add_and_multiply</span> <span class="k">class</span> <span class="nc">MyTestCase</span><span class="p">(</span><span class="n">unittest</span><span class="o">.</span><span class="n">TestCase</span><span class="p">):</span> <span class="k">def</span> <span class="nf">test_add_and_multiply</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">3</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">5</span> <span class="n">addition</span><span class="p">,</span> <span class="n">multiple</span> <span class="o">=</span> <span class="n">add_and_multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="n">addition</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="n">multiple</span><span class="p">)</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span> <span class="n">unittest</span><span class="o">.</span><span class="n">main</span><span class="p">()</span> </code></pre></div><div class="highlight"><pre><code class="language-python" data-lang="python"><span class="err">$</span> <span class="n">python</span> <span class="n">test</span><span class="o">.</span><span class="n">py</span> <span class="o">.</span> <span class="o">----------------------------------------------------------------------</span> <span class="n">Ran</span> <span class="mi">1</span> <span class="n">test</span> <span class="ow">in</span> <span class="mf">0.001</span><span class="n">s</span> <span class="n">OK</span> </code></pre></div> <p>在上面的例子中,<code>add_and_multiply</code>计算两个数的和与乘积并返回。<code>add_and_multiply</code>调用了另一个函数<code>multiply</code>进行乘积计算。</p> <p>假设我们想要摒弃“传统“的数学,并重新定义<code>multiply</code>函数,在原有的乘积结果上加3。</p> <p>新的<code>multiply</code>函数如下:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span> <span class="k">return</span> <span class="n">x</span> <span class="o">*</span> <span class="n">y</span> <span class="o">+</span> <span class="mi">3</span> </code></pre></div> <p>现在我们遇到一个问题。我们的测试代码没有变化,我们想要测试的函数也没有变化,然而,<code>test_add_and_multiply</code>却会执行失败:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="err">$</span> <span class="n">python</span> <span class="n">test</span><span class="o">.</span><span class="n">py</span> <span class="n">F</span> <span class="o">======================================================================</span> <span class="n">FAIL</span><span class="p">:</span> <span class="n">test_add_and_multiply</span> <span class="p">(</span><span class="n">__main__</span><span class="o">.</span><span class="n">MyTestCase</span><span class="p">)</span> <span class="o">----------------------------------------------------------------------</span> <span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span> <span class="n">File</span> <span class="s">&quot;test.py&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">13</span><span class="p">,</span> <span class="ow">in</span> <span class="n">test_add_and_multiply</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="n">multiple</span><span class="p">)</span> <span class="ne">AssertionError</span><span class="p">:</span> <span class="mi">15</span> <span class="o">!=</span> <span class="mi">18</span> <span class="o">----------------------------------------------------------------------</span> <span class="n">Ran</span> <span class="mi">1</span> <span class="n">test</span> <span class="ow">in</span> <span class="mf">0.001</span><span class="n">s</span> <span class="n">FAILED</span> <span class="p">(</span><span class="n">failures</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> </code></pre></div> <p>这个问题之所以会发生,是因为我们的原始测试代码并非真正的单元测试。尽管我们想要测试的是外部函数,但我们隐性的将内部函数也包含进来,因为我们期望的结果是依赖于这个内部函数的行为的。虽然在上面简单的示例中呈现的差异显得毫无意义,但某些场景下,我们需要测试一个复杂的逻辑代码块 - 例如,一个Django视图函数基于某些特定条件调用各种不同的内部功能,从函数调用结果中分离出视图逻辑的测试就显得尤为重要了。</p> <p>解决这个问题有两种方案。我们要么忽略它,像集成测试那样去进行单元测试,要么求助于mock。第一种方案的缺点是,集成测试仅仅告诉我们函数调用时哪一行代码出问题了,这样更难找到问题根源所在。这并不是说,集成测试没有用处,因为在某些情况下它确实非常有用。不管怎样,单元测试和集成测试用于解决不同的问题,它们应该被同时使用。因此,如果我们想要成为一个好的测试人员,我们会选择另一种方案:mock。</p> <h2>mock是什么?</h2> <p>mock是一个极其优秀的Python包,Python 3已将其纳入标准库。对于我们这些还在UnicodeError遍布的Python 2.x中挣扎的苦逼码农,可以通过pip进行安装:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">pip</span> <span class="n">install</span> <span class="n">mock</span><span class="o">==</span><span class="mf">1.0</span><span class="o">.</span><span class="mi">1</span> </code></pre></div> <p>mock有多种不同的用法。我们可以用它提供猴子补丁功能,创建伪造的对象,甚至可以作为一个上下文管理器。所有这些都是基于一个共同目标的,用副本替换部分代码来收集信息并返回伪造的响应。</p> <p>mock的<a href="http://www.voidspace.org.uk/python/mock/">文档</a>非常密集,寻找特定的用例信息可能会非常棘手。这里,我们就来看看一个常见的场景 - 替换一个内嵌函数并检查它的输入和输出。</p> <h2>开始mock之旅</h2> <p>让我们用mock来重新编写单元测试。接下来,我们将讨论发生了什么,以及为什么从测试的角度来看它是非常有用的:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="c"># test.py</span> <span class="kn">import</span> <span class="nn">mock</span> <span class="kn">import</span> <span class="nn">unittest</span> <span class="kn">from</span> <span class="nn">function</span> <span class="kn">import</span> <span class="n">add_and_multiply</span> <span class="k">class</span> <span class="nc">MyTestCase</span><span class="p">(</span><span class="n">unittest</span><span class="o">.</span><span class="n">TestCase</span><span class="p">):</span> <span class="nd">@mock.patch</span><span class="p">(</span><span class="s">&#39;function.multiply&#39;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">test_add_and_multiply</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">mock_multiply</span><span class="p">):</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">3</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">5</span> <span class="n">mock_multiply</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="mi">15</span> <span class="n">addition</span><span class="p">,</span> <span class="n">multiple</span> <span class="o">=</span> <span class="n">add_and_multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="n">mock_multiply</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="n">addition</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="n">multiple</span><span class="p">)</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span> <span class="n">unittest</span><span class="o">.</span><span class="n">main</span><span class="p">()</span> </code></pre></div> <p>至此,我们可以改变<code>multiply</code>函数来做任何我们想做的 - 它可能返回加3后的乘积,返回None,或返回<a href="https://www.youtube.com/watch?v=q-yxOFIkgxU&amp;t=1m15s">favourite line from Monty Python and the Holy Grail</a> - 你会发现,我们上面的测试仍然可以通过。这是因为我们mock了<code>multiply</code>函数。在真正的单元测试场景下,我们并不关心<code>multiply</code>函数内部发生了什么,从测试<code>add_and_multiply</code>的角度来看,我们只关心<code>multiply</code>被正确的参数调用了。这里我们假定有另一个单元测试会针对<code>multiply</code>的内部逻辑进行测试。</p> <h2>刚才我们做了什么?</h2> <p>咋一看,上面的语法可能不好理解。让我们逐行分析:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@mock.patch</span><span class="p">(</span><span class="s">&#39;function.multiply&#39;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">test_add_and_multiply</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">mock_multiply</span><span class="p">):</span> </code></pre></div> <p>我们使用<code>mock.patch</code>装饰器来用mock对象替换<code>multiply</code>。然后,我们将它作为一个参数<code>mock_multiply</code>插入到我们的测试代码中。在这个测试的上下文中,任何对<code>multiply</code>的调用都会被重定向到<code>mock_multiply</code>对象。</p> <p>有人会质疑 - “怎么能用对象替换函数!?“别担心!在Python的世界,函数也是对象。通常情况下,当我们调用<code>multiply()</code>,我们实际执行的是<code>multiply</code>函数的<code>__call__</code>方法。然而,恰当的使用mock,对<code>multiply()</code>的调用将执行我们的mock对象而不是<code>__call__</code>方法。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">mock_multiply</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="mi">15</span> </code></pre></div> <p>为了使mock函数可以返回任何东西,我们需要定义其<code>return_value</code>属性。实际上,当mock函数被调用时,它用于定义mock对象的返回值。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">addition</span><span class="p">,</span> <span class="n">multiple</span> <span class="o">=</span> <span class="n">add_and_multiply</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="n">mock_multiply</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span> </code></pre></div> <p>在测试代码中,我们调用了外部函数<code>add_and_multiply</code>。它会调用内嵌的<code>multiply</code>函数,如果我们正确的进行了mock,调用将会被我们定义的mock对象取代。为了验证这一点,我们可以用到mock对象的高级特性 - 当它们被调用时,传给它们的任何参数将被储存起来。顾名思义,mock对象的<code>assert_called_once_with</code>方法就是一个不错的捷径来验证某个对象是否被一组特定的参数调用过。如果被调用了,测试通过。反之,<code>assert_called_once_with</code>会抛出<code>AssertionError</code>的异常。</p> <h2>我们从中学到了什么?</h2> <p>好吧,我们遇到了很多实际问题。首先,我们通过mock将<code>multiply</code>函数从<code>add_and_multiply</code>中分离出来。这就意味着我们的单元测试只针对<code>add_and_multiply</code>的内部逻辑。只有针对<code>add_and_multiply</code>的代码修改将影响测试的成功与否。</p> <p>其次,我们现在可以控制内嵌函数的输出,以确保外部函数处理了不同的情况。例如,<code>add_and_multiply</code>可能有逻辑条件依赖于<code>multiply</code>的返回值:比如说,我们只想在乘积大于10的条件下返回一个值。通过人为设定<code>multiply</code>的返回值,我们可以模拟乘积小于10的情况以及乘积大于10的情况,从而可以很容易测试我们的逻辑正确性。</p> <p>最后,我们现在可以验证被mock的函数被调用的次数,并传入了正确的参数。由于我们的mock对象取代了<code>multiply</code>函数的位置,我们知道任何针对<code>multiply</code>函数的调用都会被重定向到该mock对象。当测试一个复杂的功能时,确保每一步都被正确调用将是一件非常令人欣慰的事情。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Ruby Profiler详解之ruby-prof(I) /ruby/2015/06/08/ruby-prof/ Mon, 08 Jun 2015 00:00:00 +0000 /ruby/2015/06/08/ruby-prof <p>项目地址: <a href="https://github.com/ruby-prof/ruby-prof">ruby-prof</a></p> <p>在上一篇<a href="http://code.oneapm.com/ruby/2015/04/08/ruby-profilers/">Ruby中的Profiling工具</a>中,我们列举了几种 最常用的Profiler,不过只是简单介绍,这一次详细介绍一下ruby-prof的使用方法。</p> <p>ruby-prof是比较强大的,支持cpu,内存使用,对象分配等等的性能分析,而且提供了很多友好的输出格式,不仅仅是 有基于文字,html的格式,还能输出graphviz格式的dot文件,以及适用与<a href="http://kcachegrind.sourceforge.net/html/Home.html">KCacheGrind</a>的<code>call tree</code>格式, 其实这个格式是基于<a href="http://valgrind.org/">Valgrind</a>的,这个工具很棒,大家可以去官网了解一下。</p> <p>有两种方式运行<code>ruby-prof</code>,一种是需要在源码中插入<code>ruby-prof</code>的启动和停止代码:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">&#39;ruby-prof&#39;</span> <span class="no">RubyProf</span><span class="o">.</span><span class="n">start</span> <span class="c1"># 这里写入要进行性能剖析的代码</span> <span class="n">result</span> <span class="o">=</span> <span class="no">RubyProf</span><span class="o">.</span><span class="n">stop</span> <span class="c1"># 选择一个Printer</span> <span class="n">printer</span> <span class="o">=</span> <span class="no">RubyProf</span><span class="o">::</span><span class="no">FlatPrinter</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="n">printer</span><span class="o">.</span><span class="n">print</span><span class="p">(</span><span class="no">STDOUT</span><span class="p">)</span> </code></pre></div> <p>还有一种是在命令行直接运行的,安装了Gem包<code>ruby-prof</code>之后,会同时安装<code>ruby-prof</code>命令,使用如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof -p flat test.rb </code></pre></div> <p>这种方法更灵活,我们使用这种方法来说明<code>ruby-prof</code>的使用方法。</p> <p>直接运行<code>ruby-prof -h</code>得到<code>ruby-prof</code>的帮助信息,由于太多,这里就不列出来了,大家可以自己在系统中执行看看。</p> <p>其中<code>-p</code>参数为输出格式,以下就会逐一介绍各个Printer的格式,指数的意义以及相关显示工具的使用。在介绍输出格式的过程中,也会相应的介绍其他的几个参数的用途。</p> <h2>输出格式类型</h2> <div class="highlight"><pre><code class="language-text" data-lang="text">flat - Prints a flat profile as text (default). flat_with_line_numbers - same as flat, with line numbers. graph - Prints a graph profile as text. graph_html - Prints a graph profile as html. call_tree - format for KCacheGrind call_stack - prints a HTML visualization of the call tree dot - Prints a graph profile as a dot file multi - Creates several reports in output directory </code></pre></div> <h2>示例程序</h2> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="s2">&quot;string&quot;</span> <span class="o">*</span> <span class="mi">1000</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">m2</span> <span class="s2">&quot;string&quot;</span> <span class="o">*</span> <span class="mi">10000</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">start</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">0</span> <span class="n">n</span> <span class="o">=</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">while</span> <span class="n">n</span> <span class="o">&lt;</span> <span class="mi">100_000</span> <span class="mi">10000</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span> <span class="n">m1</span> <span class="n">m2</span> <span class="k">end</span> <span class="k">end</span> <span class="n">start</span> </code></pre></div> <p>这是最基础的测试程序,我们会在介绍<code>ruby-prof</code>的功能的同时添加其他代码来进行演示。</p> <h2>GC对性能剖析的影响</h2> <p>进行性能剖析的时候GC的运行总会对结果产生比较大的影响,这里我们暂时不考虑它,我们会有另外一篇文章 做专门的介绍。</p> <h2>最简单的输出格式 - flat</h2> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof -p flat test.rb </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">Measure Mode: wall_time Thread ID: 12161840 Fiber ID: 19223800 Total: 0.206998 Sort by: self_time %self total self wait child calls name 68.50 0.142 0.142 0.000 0.000 20000 String#* 10.45 0.207 0.022 0.000 0.185 1 Object#start 6.82 0.014 0.014 0.000 0.000 100001 Fixnum#&lt; 6.46 0.013 0.013 0.000 0.000 100000 Fixnum#+ 2.84 0.158 0.006 0.000 0.152 1 Integer#times 2.52 0.128 0.005 0.000 0.123 10000 Object#m2 2.40 0.024 0.005 0.000 0.019 10000 Object#m1 0.01 0.207 0.000 0.000 0.207 2 Global#[No method] 0.01 0.000 0.000 0.000 0.000 2 IO#set_encoding 0.00 0.000 0.000 0.000 0.000 3 Module#method_added * indicates recursively called methods </code></pre></div> <p>先来一一解释一下各项指标的意思:</p> <table><thead> <tr> <th style="text-align: right">Indicator</th> <th style="text-align: left">Explanation</th> </tr> </thead><tbody> <tr> <td style="text-align: right"><strong>%self</strong></td> <td style="text-align: left">方法本身执行的时间占比,不包括调用的其他的方法执行时间</td> </tr> <tr> <td style="text-align: right"><strong>total</strong></td> <td style="text-align: left">方法执行的总时间,包括调用的其他方法的执行时间</td> </tr> <tr> <td style="text-align: right"><strong>self</strong></td> <td style="text-align: left">方法本身执行的时间,不包括调用的其他的方法执行时间</td> </tr> <tr> <td style="text-align: right"><strong>wait</strong></td> <td style="text-align: left">多线程中,等待其他线程的时间,在单线程程序中,始终为0</td> </tr> <tr> <td style="text-align: right"><strong>child</strong></td> <td style="text-align: left">方法调用的其他方法的总时间</td> </tr> <tr> <td style="text-align: right"><strong>calls</strong></td> <td style="text-align: left">方法的调用次数</td> </tr> </tbody></table> <p>他们之间的基本关系就是:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">total = self + wait + child </code></pre></div> <p>具体来说就是<code>String#*</code>这个方法占据程序运行时间的68.50%,花费了0.142秒,执行了20000次,而 <code>Object#start</code>方法就是代码中定义的<code>start</code>方法,它占据程序运行时间的10.45%,花费了0.022秒,调用的 方法花费了0.185秒,调用了1次,总共花费的时间(total)为0.022 + 0.185 = 0.207,相信现在大家都能明 白这些指数的意义了。</p> <p>现在我们明白了这个输出的指标意思,假如这个程序是存在性能问题的,那么这些数据说明了什么问题?通常情况下, 我们需要看两个指标,%self和calls,单纯看%self有时候是没有用的,上面这个例子,它的耗时方法是<code>String#*</code>, 我们不太可能去改进语言本身的方法,这种情况下,我们发现calls的值比较大,那么就想办法减少对<code>String#*</code>的方法调用。</p> <p>利用flat输出格式,也就只能发现这样简单的问题,如果这时候想要减少<code>String#*</code>的方法调用,就需要知道是谁调用了它, 而这个输出格式是体现不出来的,就需要选择其他的输出格式。</p> <h2>简单的调用关系输出 - graph</h2> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof -p graph test.rb </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text">Measure Mode: wall_time Thread ID: 17371960 Fiber ID: 24397420 Total Time: 0.21026015281677246 Sort by: total_time %total %self total self wait child calls Name -------------------------------------------------------------------------------- 99.99% 0.01% 0.210 0.000 0.000 0.210 2 Global#[No method] 0.210 0.022 0.000 0.188 1/1 Object#start 0.000 0.000 0.000 0.000 3/3 Module#method_added -------------------------------------------------------------------------------- 0.210 0.022 0.000 0.188 1/1 Global#[No method] 99.98% 10.34% 0.210 0.022 0.000 0.188 1 Object#start 0.161 0.006 0.000 0.155 1/1 Integer#times 0.014 0.014 0.000 0.000 100001/100001 Fixnum#&lt; 0.014 0.014 0.000 0.000 100000/100000 Fixnum#+ -------------------------------------------------------------------------------- 0.161 0.006 0.000 0.155 1/1 Object#start 76.48% 2.68% 0.161 0.006 0.000 0.155 1 Integer#times 0.130 0.005 0.000 0.125 10000/10000 Object#m2 0.025 0.005 0.000 0.020 10000/10000 Object#m1 -------------------------------------------------------------------------------- 0.020 0.020 0.000 0.000 10000/20000 Object#m1 0.125 0.125 0.000 0.000 10000/20000 Object#m2 69.23% 69.23% 0.146 0.146 0.000 0.000 20000 String#* -------------------------------------------------------------------------------- 0.130 0.005 0.000 0.125 10000/10000 Integer#times 61.81% 2.28% 0.130 0.005 0.000 0.125 10000 Object#m2 0.125 0.125 0.000 0.000 10000/20000 String#* -------------------------------------------------------------------------------- 0.025 0.005 0.000 0.020 10000/10000 Integer#times 11.99% 2.28% 0.025 0.005 0.000 0.020 10000 Object#m1 0.020 0.020 0.000 0.000 10000/20000 String#* -------------------------------------------------------------------------------- 0.014 0.014 0.000 0.000 100001/100001 Object#start 6.73% 6.73% 0.014 0.014 0.000 0.000 100001 Fixnum#&lt; -------------------------------------------------------------------------------- 0.014 0.014 0.000 0.000 100000/100000 Object#start 6.42% 6.42% 0.014 0.014 0.000 0.000 100000 Fixnum#+ -------------------------------------------------------------------------------- 0.01% 0.01% 0.000 0.000 0.000 0.000 2 IO#set_encoding -------------------------------------------------------------------------------- 0.000 0.000 0.000 0.000 3/3 Global#[No method] 0.00% 0.00% 0.000 0.000 0.000 0.000 3 Module#method_added * indicates recursively called methods </code></pre></div> <p>这次输出的内容就比较丰富,不过也可能让人头有点晕。我们来慢慢分析一下。</p> <p>首先这次排序方式不一样了,是按照<code>total_time</code>排序的,flat输出格式是按照<code>self_time</code>排序的。整个报告被虚线分割为 几部分,每部分中都描述了不定个数的方法调用信息,但是注意最左边两列,就是<code>%total</code>, <code>%self</code>那两列不为空的那一行,</p> <p>先来看第二部分:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">-------------------------------------------------------------------------------- 0.210 0.022 0.000 0.188 1/1 Global#[No method] 99.98% 10.34% 0.210 0.022 0.000 0.188 1 Object#start 0.161 0.006 0.000 0.155 1/1 Integer#times 0.014 0.014 0.000 0.000 100001/100001 Fixnum#&lt; 0.014 0.014 0.000 0.000 100000/100000 Fixnum#+ -------------------------------------------------------------------------------- </code></pre></div> <p><code>Object#start</code>方法的执行花费了99.98%的总时间,不包括子方法调用的话,花费了10.34%的时间,调用了 一次,并且在<code>start</code>方法中还调用了<code>Integer#times</code>、<code>Fixnum#&lt;</code>和<code>Fixnum#+</code>三个方法。</p> <p>再来看右数第二列(calls),是被<code>/</code>分隔的两个数,左边的数是此方法在这一层级调用了多少次<code>Object#start</code>,右边的数是 <code>Object#start</code>这个程序运行过程中总的运行次数。而<code>Object#start</code>调用的三个方法<code>calls</code>列出的是在<code>Object#start</code> 中执行的次数,以及总的执行次数。</p> <p>最开始的一部分中有这样两个方法:<code>Global#[No method]</code>代表没有caller,可以理解为ruby正在准备执行环境, <code>Module#method_added</code>是当有实例方法添加的时候,这个方法都会被触发。</p> <p>那么这种输出格式能解释什么问题呢?在flat输出格式中我们已经定位到了问题是<code>String#*</code>的调用次数太多, 那么根据这个graph格式的输出格式我们应该可以找到是谁导致的这个问题。</p> <p>先把可以发现问题的部分截出来:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">-------------------------------------------------------------------------------- 0.020 0.020 0.000 0.000 10000/20000 Object#m1 0.125 0.125 0.000 0.000 10000/20000 Object#m2 69.23% 69.23% 0.146 0.146 0.000 0.000 20000 String#* -------------------------------------------------------------------------------- 0.130 0.005 0.000 0.125 10000/10000 Integer#times 61.81% 2.28% 0.130 0.005 0.000 0.125 10000 Object#m2 0.125 0.125 0.000 0.000 10000/20000 String#* -------------------------------------------------------------------------------- 0.025 0.005 0.000 0.020 10000/10000 Integer#times 11.99% 2.28% 0.025 0.005 0.000 0.020 10000 Object#m1 0.020 0.020 0.000 0.000 10000/20000 String#* -------------------------------------------------------------------------------- </code></pre></div> <p>第一部分说明<code>String#*</code>在<code>Object#m1</code>和<code>Object#m1</code>中各被调用了10000次,一共执行了20000次,次数一样,接着看下面, 同样是10000次,在<code>Object#m2</code>中花费的时间是0.125秒,而在<code>Object#m1</code>中花费的时间是0.020秒,多出了0.105秒,这样, 我们能定位出问题出在了<code>Object#m2</code>这里。</p> <p>graph可输出为html格式,这里只是演示了纯文本版,html格式更容易交互,需要添加参数<code>-f</code>指定输出的路径和文件名。</p> <h2>GraphViz dot - dot</h2> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof -p dot test.rb -f dot.dot </code></pre></div> <p>有工具可以将dot文件转换为pdf查看,也有专门查看dot文件的工具,比如ubuntu上的XDot。</p> <p><img src="/images/ruby-prof-dot.png" width="100%" style="border: 1px solid #eee;" /></p> <p>这张图也明确说明了问题出在了<code>Object#m2</code>这里。</p> <h2>可交互的调用关系 - call_stack</h2> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof -p call_stack test.rb -f callstack.html </code></pre></div> <p><img src="/images/ruby-prof-call-stack.png" width="100%" /></p> <p>这里真是一图胜千言,一目了然,<code>Object#m2</code>中的<code>String#*</code>的10000次调用花费了60.52%的时间,不用多解释,快点自己尝试一下吧。</p> <h2>终极万能全视角 - call_tree</h2> <p>首先安装<code>KCacheGrind</code>,ubuntu下直接&#39;sudo apt-get install kcachegrind&#39;</p> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof -p call_tree test.rb -f call_tree.out </code></pre></div> <p>打开<code>KCacheGrind</code>,然后打开<code>call_tree.out</code>(文件类型选所有),这个神奇的工具能呈现给你所有真相。</p> <p><img src="/images/ruby-prof-grind.png" width="100%" style="border: 1px solid #eee;" /> <img src="/images/ruby-prof-grind-1.png" width="100%" style="border: 1px solid #eee;"/> <img src="/images/ruby-prof-grind-2.png" width="100%" style="border: 1px solid #eee;"/> <img src="/images/ruby-prof-grind-3.png" width="100%" style="border: 1px solid #eee;"/> <img src="/images/ruby-prof-grind-4.png" width="100%" style="border: 1px solid #eee;"/></p> <p>有了前面介绍的输出格式说明,看懂这个就很容易了,我们还是会介绍一下,不过是在另一篇,因为这篇有点太长了,下一篇 会详细介绍一下KCacheGrind的使用方法。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Android开发者:你真的会用AsyncTask吗? /android/2015/06/02/android1/ Tue, 02 Jun 2015 00:00:00 +0000 /android/2015/06/02/android1 <h1>Android开发者:你真的会用AsyncTask吗?</h1> <p>【<strong>导读</strong>】在Android应用开发的过程中,我们需要时刻注意保证应用程序的稳定和UI操作响应及时,因为不稳定或响应缓慢的应用将给应用带来不好的印象, 严重的用户卸载你的APP,这样你的努力就没有体现的价值了。本文试图从AsnycTask的作用说起,进一步的讲解一下内部的实现机制。如果有一些开发经验的人, 读完之后应该对使用AsnycTask过程中的一些问题豁然开朗,开发经验不丰富的也可以从中找到使用过程中的注意点。</p> <h2>为何引入AsnyncTask?</h2> <p>在Android程序开始运行的时候会单独启动一个进程,默认情况下所有这个程序操作都在这个进程中进行。一个Android程序默认情况下只有一个进程,但是一个进程却是可以有许线程的。</p> <p>在这些线程中,有一个线程叫做UI线程,也叫做Main Thread,除了Main Thread之外的线程都可称为Worker Thread。Main Thread主要负责控制UI页面的显示、更新、交互等。 因此所有在UI线程中的操作要求越短越好,只有这样用户才会觉得操作比较流畅。一个比较好的做法是把一些比较耗时的操作,例如网络请求、数据库操作、 复杂计算等逻辑都封装到单独的线程,这样就可以避免阻塞主线程。为此,有人写了如下的代码:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">private TextView textView; public void onCreate(Bundle bundle){ super.onCreate(bundle); setContentView(R.layout.thread_on_ui); textView = (TextView) findViewById(R.id.tvTest); new Thread(new Runnable() { @Override public void run() { try { HttpGet httpGet = new HttpGet(&quot;http://www.baidu.com&quot;); HttpClient httpClient = new DefaultHttpClient(); HttpResponse httpResp = httpClient.execute(httpGet); if (httpResp.getStatusLine().getStatusCode() == 200) { String result = EntityUtils.toString(httpResp.getEntity(), &quot;UTF-8&quot;); textView.setText(&quot;请求返回正常,结果是:&quot; + result); } else { textView.setText(&quot;请求返回异常!&quot;); } }catch (IOException e){ e.printStackTrace(); } } }).start(); } </code></pre></div> <p>运行,不出所料,异常信息如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. </code></pre></div> <p>怎么破?可以在主线程创建Handler对象,把textView.setText地方替换为用handler把返回值发回到handler所在的线程处理,也就是主线程。 这个处理方法稍显复杂,Android为我们考虑到了这个情况,给我们提供了一个轻量级的异步类可以直接继承AsyncTask,在类中实现异步操作, 并提供接口反馈当前异步执行的结果以及执行进度,这些接口中有直接运行在主线程中的,例如onPostExecute,onPreExecute等方法。</p> <p>也就是说,Android的程序运行时是多线程的,为了更方便的处理子线程和UI线程的交互,引入了AsyncTask。</p> <h2>AsnyncTask内部机制</h2> <p>AsyncTask内部逻辑主要有二个部分:</p> <p>1、<strong>与主线的交互</strong>,它内部实例化了一个静态的自定义类InternalHandler,这个类是继承自Handler的,在这个自定义类中绑定了一个叫做AsyncTaskResult的对象,每次子线程需要通知主线程,就调用sendToTarget发送消息给handler。然后在handler的handleMessage中AsyncTaskResult根据消息的类型不同(例如MESSAGE<em>POST</em>PROGRESS会更新进度条,MESSAGE<em>POST</em>CANCEL取消任务)而做不同的操作,值得一提的是,这些操作都是在UI线程进行的,意味着,从子线程一旦需要和UI线程交互,内部自动调用了handler对象把消息放在了主线程了。<a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.7_r1/android/os/AsyncTask.java#AsyncTask"><strong>源码地址</strong></a></p> <div class="highlight"><pre><code class="language-text" data-lang="text"> mFuture = new FutureTask&lt;Result&gt;(mWorker) { @Override protected void More ...done() { Message message; Result result = null; try { result = get(); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException(&quot;An error occured while executing doInBackground()&quot;, e.getCause()); } catch (CancellationException e) { message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, new AsyncTaskResult&lt;Result&gt;(AsyncTask.this, (Result[]) null)); message.sendToTarget(); return; } catch (Throwable t) { throw new RuntimeException(&quot;An error occured while executing &quot; + &quot;doInBackground()&quot;, t); } message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult&lt;Result&gt;(AsyncTask.this, result)); message.sendToTarget(); } }; </code></pre></div><div class="highlight"><pre><code class="language-text" data-lang="text"> private static class InternalHandler extends Handler { @SuppressWarnings({&quot;unchecked&quot;, &quot;RawUseOfParameterizedType&quot;}) @Override public void More ...handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: // There is only one result result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; case MESSAGE_POST_CANCEL: result.mTask.onCancelled(); break; } } } </code></pre></div> <p>2、<strong>AsyncTask内部调度</strong>,虽然可以新建多个AsyncTask的子类的实例,但是AsyncTask的内部Handler和ThreadPoolExecutor都是static的, 这么定义的变量属于类的,是进程范围内共享的,所以AsyncTask控制着进程范围内所有的子类实例,而且该类的所有实例都共用一个线程池和Handler。代码如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">public abstract class AsyncTask&lt;Params, Progress, Result&gt; { private static final String LOG_TAG = &quot;AsyncTask&quot;; private static final int CORE_POOL_SIZE = 5; private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 1; private static final BlockingQueue&lt;Runnable&gt; sWorkQueue = new LinkedBlockingQueue&lt;Runnable&gt;(10); private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread More ...newThread(Runnable r) { return new Thread(r, &quot;AsyncTask #&quot; + mCount.getAndIncrement()); } }; private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory); private static final int MESSAGE_POST_RESULT = 0x1; private static final int MESSAGE_POST_PROGRESS = 0x2; private static final int MESSAGE_POST_CANCEL = 0x3; </code></pre></div> <p>从代码还可以看出,默认核心线程池的大小是5,缓存任务队列是10。意味着,如果线程池的线程数量小于5,这个时候新添加一个异步任务则会新建一个线程; 如果线程池的数量大于等于5,这个时候新建一个异步任务这个任务会被放入缓存队列中等待执行。限制一个APP内AsyncTask并发的线程的数量看似是有必要的, 但也带来了一个问题,假如有人就是需要同时运行10个而不是5个,或者不对线程的多少做限制,例如有些APP的瀑布流页面中的N多图片的加载。</p> <p>另一方面,同时运行的任务多,线程也就多,如果这些任务是去访问网络的,会导致短时间内手机那可怜的带宽被占完了,这样总体的表现是谁都很难很快加载完全, 因为他们是竞争关系。所以,把选择权交给开发者吧。</p> <p>事实上,大概从Android从3.0开始,每次新建异步任务的时候AsnycTask内部默认规则是按提交的先后顺序每次只运行一个异步任务。当然了你也可以自己指定自己的线程池。</p> <p>可以看出,AsyncTask使用过程中需要注意的地方不少</p> <ul> <li>由于Handler需要和主线程交互,而Handler又是内置于AsnycTask中的,所以,AsyncTask的创建必须在主线程。</li> <li>AsyncTaskResult的doInBackground(mParams)方法执行异步任务运行在子线程中,其他方法运行在主线程中,可以操作UI组件。</li> <li>不要手动的去调用AsyncTask的onPreExecute, doInBackground, publishProgress, onProgressUpdate, onPostExecute方法,这些都是由Android系统自动调用的</li> <li>一个任务AsyncTask任务只能被执行一次。</li> <li>运行中可以随时调用cancel(boolean)方法取消任务,如果成功调用isCancelled()会返回true,并且不会执行onPostExecute() 方法了,取而代之的是调用 onCancelled() 方法。而且从源码看,如果这个任务已经执行了这个时候调用cancel是不会真正的把task结束,而是继续执行,只不过改变的是执行之后的回调方法是onPostExecute还是onCancelled。</li> </ul> <h2>AsnyncTask和Activity OnConfiguration</h2> <p>上面提到了那么多的注意点,还有其他需要注意的吗?当然有!我们开发App过程中使用AsyncTask请求网络数据的时候,一般都是习惯在onPreExecute显示进度条, 在数据请求完成之后的onPostExecute关闭进度条。这样做看似完美,但是如果您的App没有明确指定屏幕方向和configChanges时,当用户旋转屏幕的时候Activity就会重新启动, 而这个时候您的异步加载数据的线程可能正在请求网络。当一个新的Activity被重新创建之后,可能由重新启动了一个新的任务去请求网络,这样之前的一个异步任务不经意间就泄露了, 假设你还在onPostExecute写了一些其他逻辑,这个时候就会发生意想不到异常。</p> <p>一般简单的数据类型的,对付configChanges我们很好处理,我们直接可以通过onSaveInstanceState()和onRestoreInstanceState()进行保存与恢复。 Android会在销毁你的Activity之前调用onSaveInstanceState()方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在onCreate()或onRestoreInstanceState()方法中恢复。</p> <p>但是,对于AsyncTask怎么办?问题产生的根源在于Activity销毁重新创建的过程中AsyncTask和之前的Activity失联,最终导致一些问题。 那么解决问题的思路也可以朝着这个方向发展。<a href="https://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance">Android官方文档</a> 也有一些解决问题的线索。</p> <p>这里介绍另外一种使用事件总线的解决方案,是国外一个安卓大牛写的。中间用到了Square开源的EventBus类库http://square.github.io/otto/。 首先自定义一个AsyncTask的子类,在onPostExecute方法中,把返回结果抛给事件总线,代码如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text"> @Override protected String doInBackground(Void... params) { Random random = new Random(); final long sleep = random.nextInt(10); try { Thread.sleep(10 * 6000); } catch (InterruptedException e) { e.printStackTrace(); } return &quot;Slept for &quot; + sleep + &quot; seconds&quot;; } @Override protected void onPostExecute(String result) { MyBus.getInstance().post(new AsyncTaskResultEvent(result)); } </code></pre></div> <p>在Activity的onCreate中注册这个事件总线,这样异步线程的消息就会被otta分发到当前注册的activity,这个时候返回结果就在当前activity的onAsyncTaskResult中了,代码如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text"> @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.otto_layout); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new MyAsyncTask().execute(); } }); MyBus.getInstance().register(this); } @Override protected void onDestroy() { MyBus.getInstance().unregister(this); super.onDestroy(); } @Subscribe public void onAsyncTaskResult(AsyncTaskResultEvent event) { Toast.makeText(this, event.getResult(), Toast.LENGTH_LONG).show(); } </code></pre></div> <p>个人觉的这个方法相当好,当然更简单的你也可以不用otta这个库,自己单独的用接口回调的方式估计也能实现,大家可以试试。</p> <p><strong>本文系<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创。想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Rake::TestTask 介绍 /ruby/2015/05/28/ruby-rake-testtask/ Thu, 28 May 2015 00:00:00 +0000 /ruby/2015/05/28/ruby-rake-testtask <p>通常我们创建一个新的项目的时候,会建立一个<code>test</code>或者<code>spec</code>的文件夹来存放测试的文件,运行这些测试需要单独的命令,比如在项目目录下执行<code>rspec .</code>或者<code>ruby test.rb</code>等等,这些测试的工具中大多也都提供了更方便的方式来运行这些测试,但是如果你喜欢用<code>Rake</code>的话,那就有另外一个选择<code>Rake::TestTask</code>。</p> <p>先上代码:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">&quot;rake/testtask&quot;</span> <span class="no">Rake</span><span class="o">::</span><span class="no">TestTask</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="o">.</span><span class="n">libs</span> <span class="o">&lt;&lt;</span> <span class="s2">&quot;test&quot;</span> <span class="n">t</span><span class="o">.</span><span class="n">pattern</span> <span class="o">=</span> <span class="s2">&quot;test/*_test.rb&quot;</span> <span class="n">t</span><span class="o">.</span><span class="n">verbose</span> <span class="o">=</span> <span class="kp">true</span> <span class="k">end</span> </code></pre></div> <p>将以上代码保存或添加到项目目录中的<code>Rakefile</code>,然后在这个目录下执行<code>rake test</code>,你就可以看到如下输出(这里我用的测试框架是<code>Minitest</code>):</p> <div class="highlight"><pre><code class="language-text" data-lang="text">/home/lizhe/.rvm/rubies/ruby-2.2.2/bin/ruby -I&quot;lib:test&quot; &quot;/home/lizhe/.rvm/rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb&quot; &quot;test/person_test.rb&quot; Run options: --seed 535 # Running: . Finished in 0.000558s, 1793.5482 runs/s, 1793.5482 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips </code></pre></div> <p>如果去掉最上面代码中的<code>t.verbose = true</code>的话:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">&quot;rake/testtask&quot;</span> <span class="no">Rake</span><span class="o">::</span><span class="no">TestTask</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span> <span class="n">t</span><span class="o">.</span><span class="n">libs</span> <span class="o">&lt;&lt;</span> <span class="s2">&quot;test&quot;</span> <span class="n">t</span><span class="o">.</span><span class="n">pattern</span> <span class="o">=</span> <span class="s2">&quot;test/*_test.rb&quot;</span> <span class="k">end</span> </code></pre></div> <p>则输出结果就不会显示执行的ruby命令,如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Run options: --seed 17527 # Running: . Finished in 0.001629s, 613.9689 runs/s, 613.9689 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips </code></pre></div> <p>这里再介绍一下<code>Rake::TestTask</code>的配置选项:</p> <ul> <li><code>libs</code>会被添加到<code>$LOAD_PATH</code>中;</li> <li><code>pattern</code> 测试文件的名称匹配,还有另外一个叫做<code>test_files</code>,这两个使用哪一个都可以,如果同时使用的话,会合并两者;</li> <li><code>verbose</code> 输出这个<code>Rake::TestTask</code>执行的具体Ruby命令;</li> <li><code>options</code> 指定给测试框架的参数,类型为数组,也可以通过传入<code>TESTOPTS</code>命令行参数达到相同的目的;</li> <li><code>ruby_opts</code> 执行ruby命令时的参数,类型为数组,比如可以在指定为<code>-rtesthelper</code>,避免每个测试文件重复<code>require &quot;test_helper&quot;</code>;</li> <li><code>name</code> <code>Rake Task</code>的名称,默认是<code>test</code>;</li> </ul> <p>还有一点,在我们执行<code>rake test</code>的时候,是可以传入一些参数的,比如我执行执行<code>test/dummy_test.rb</code>这个测试,那么就可以指定<code>TEST</code>参数:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">rake test TEST=test/dummy_test.rb </code></pre></div> <p>还可以通过<code>TESTOPTS</code>给你使用的测试框架传入参数,比如<code>Minitest</code>有<code>-v</code>和<code>-p</code>两个参数,那么就可以这样传进去:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">rake test TESTOPTS=&quot;-vp&quot; </code></pre></div> <p>参考链接:</p> <ul> <li><a href="http://ruby-doc.org/stdlib-2.2.2/libdoc/rake/rdoc/Rake/TestTask.html">http://ruby-doc.org/stdlib-2.2.2/libdoc/rake/rdoc/Rake/TestTask.html</a></li> </ul> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】从底层理解 Python 的执行 /python/2015/05/27/python-understand/ Wed, 27 May 2015 00:00:00 +0000 /python/2015/05/27/python-understand <p>原文地址:<a href="http://blog.hakril.net/articles/2-understanding-python-execution-tracer.html">Understanding Python execution from inside: A Python assembly tracer</a></p> <p>最近我在学习 Python 的运行模型。我对 Python 的一些内部机制很是好奇,比如 Python 是怎么实现类似 YIELD<em>VALUE、YIELD</em>FROM 这样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣的 Python 特性是怎么编译的;从字节码的层面来看,当异常抛出的时候都发生了什么事情。翻阅 CPython 的代码对于解答这些问题当然是很有帮助的,但我仍然觉得以这样的方式来做的话对于理解字节码的执行和堆栈的变化还是缺少点神马。GDB 是个好选择,但是我懒而且只想使用一些比较高阶的接口写点 Python 代码来完成这件事。</p> <p>所以呢,我的目标就是创建一个字节码级别的追踪 API,类似 sys.setrace 所提供的那样,但相对而言会有更好的粒度。这充分锻炼了我编写 Python 实现的 C 代码的编码能力。</p> <p>我们所需要的有如下几项</p> <ul> <li>一个新的 Cpython 解释器操作码</li> <li>一种将操作码注入到 Python 字节码的方法</li> <li>一些用于处理操作码的 Python 代码</li> </ul> <p>在这篇文章中所用的 Python 版本为 3.5</p> <h2>一个新的 Cpython 操作码</h2> <h3>新操作码:DEBUG_OP</h3> <p>这个新的操作码 DEBUG_OP 是我第一次尝试写 CPython 实现的 C 代码,我将尽可能的让它保持简单。 我们想要达成的目的是当我们的操作码被执行的时候我能有一种方式来调用一些 Python 代码。同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。通过操作码能辨识出的有用信息如下:</p> <ul> <li>堆栈的内容</li> <li>执行 DEBUG_OP 的帧对象信息</li> </ul> <p>所以呢,我们的操作码需要做的事情是</p> <ul> <li>找到回调函数</li> <li>创建一个包含堆栈内容的列表</li> <li>调用回调函数,并将包含堆栈内容的列表和当前帧作为参数传递给它</li> </ul> <p>听起来挺简单的,现在开始动手吧! <strong>声明</strong>:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论 首先要做的是给操作码定义一个名字和相应的值,因此我们需要在 <a href="https://github.com/python/cpython/blob/master/Include/opcode.h">Include/opcode.h</a> 中添加代码。</p> <div class="highlight"><pre><code class="language-c" data-lang="c"><span class="cm">/** My own comments begin by &#39;**&#39; **/</span> <span class="cm">/** From: Includes/opcode.h **/</span> <span class="cm">/* Instruction opcodes for compiled code */</span> <span class="cm">/** We just have to define our opcode with a free value</span> <span class="cm"> 0 was the first one I found **/</span> <span class="cp">#define DEBUG_OP 0</span> <span class="cp">#define POP_TOP 1</span> <span class="cp">#define ROT_TWO 2</span> <span class="cp">#define ROT_THREE 3</span> </code></pre></div> <p>这部分工作就完成了,现在我们去编写操作码真正干活的代码。</p> <h3>实现 DEBUG_OP</h3> <p>在考虑如何实现 <code>DEBUG_OP</code> 之前我们需要了解的是 <code>DEBUG_OP</code> 提供的接口将长什么样。 拥有一个可以调用其他代码的新操作码是相当酷眩叼炸天的,但是究竟它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种最简单的方法:在帧的全局区域写死函数名。</p> <p>那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?</p> <p>为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符 <code>__enter__</code> 和 <code>__exit__</code></p> <p>我们可以看到这两标识符被使用在操作码 <code>SETUP_WITH</code> 中</p> <div class="highlight"><pre><code class="language-C" data-lang="C"><span class="cm">/** From: Python/ceval.c **/</span> <span class="n">TARGET</span><span class="p">(</span><span class="n">SETUP_WITH</span><span class="p">)</span> <span class="p">{</span> <span class="n">_Py_IDENTIFIER</span><span class="p">(</span><span class="n">__exit__</span><span class="p">);</span> <span class="n">_Py_IDENTIFIER</span><span class="p">(</span><span class="n">__enter__</span><span class="p">);</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">mgr</span> <span class="o">=</span> <span class="n">TOP</span><span class="p">();</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">exit</span> <span class="o">=</span> <span class="n">special_lookup</span><span class="p">(</span><span class="n">mgr</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">PyId___exit__</span><span class="p">),</span> <span class="o">*</span><span class="n">enter</span><span class="p">;</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">res</span><span class="p">;</span> </code></pre></div> <p>现在,看一眼宏 <code>_Py_IDENTIFIER</code> 的定义</p> <div class="highlight"><pre><code class="language-C" data-lang="C"><span class="cm">/** From: Include/object.h **/</span> <span class="cm">/********************* String Literals ****************************************/</span> <span class="cm">/* This structure helps managing static strings. The basic usage goes like this:</span> <span class="cm"> Instead of doing</span> <span class="cm"> r = PyObject_CallMethod(o, &quot;foo&quot;, &quot;args&quot;, ...);</span> <span class="cm"> do</span> <span class="cm"> _Py_IDENTIFIER(foo);</span> <span class="cm"> ...</span> <span class="cm"> r = _PyObject_CallMethodId(o, &amp;PyId_foo, &quot;args&quot;, ...);</span> <span class="cm"> PyId_foo is a static variable, either on block level or file level. On first</span> <span class="cm"> usage, the string &quot;foo&quot; is interned, and the structures are linked. On interpreter</span> <span class="cm"> shutdown, all strings are released (through _PyUnicode_ClearStaticStrings).</span> <span class="cm"> Alternatively, _Py_static_string allows to choose the variable name.</span> <span class="cm"> _PyUnicode_FromId returns a borrowed reference to the interned string.</span> <span class="cm"> _PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*.</span> <span class="cm">*/</span> <span class="k">typedef</span> <span class="k">struct</span> <span class="n">_Py_Identifier</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">_Py_Identifier</span> <span class="o">*</span><span class="n">next</span><span class="p">;</span> <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">string</span><span class="p">;</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">object</span><span class="p">;</span> <span class="p">}</span> <span class="n">_Py_Identifier</span><span class="p">;</span> <span class="cp">#define _Py_static_string_init(value) { 0, value, 0 }</span> <span class="cp">#define _Py_static_string(varname, value) static _Py_Identifier varname = _Py_static_string_init(value)</span> <span class="cp">#define _Py_IDENTIFIER(varname) _Py_static_string(PyId_##varname, #varname)</span> </code></pre></div> <p>嗯,注释部分已经说明得很清楚了。通过一番查找,我们发现了可以用来从字典找固定字符串的函数 <code>_PyDict_GetItemId</code> 所以我们操作码的查找部分的代码就是长这样滴</p> <div class="highlight"><pre><code class="language-C" data-lang="C"> <span class="cm">/** Our callback function will be named op_target **/</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">target</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="n">_Py_IDENTIFIER</span><span class="p">(</span><span class="n">op_target</span><span class="p">);</span> <span class="n">target</span> <span class="o">=</span> <span class="n">_PyDict_GetItemId</span><span class="p">(</span><span class="n">f</span><span class="o">-&gt;</span><span class="n">f_globals</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">PyId_op_target</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">target</span> <span class="o">==</span> <span class="nb">NULL</span> <span class="o">&amp;&amp;</span> <span class="n">_PyErr_OCCURRED</span><span class="p">())</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">PyErr_ExceptionMatches</span><span class="p">(</span><span class="n">PyExc_KeyError</span><span class="p">))</span> <span class="k">goto</span> <span class="n">error</span><span class="p">;</span> <span class="n">PyErr_Clear</span><span class="p">();</span> <span class="n">DISPATCH</span><span class="p">();</span> <span class="p">}</span> </code></pre></div> <p>为了方便理解,对这一段代码做一些说明</p> <ul> <li><code>f</code> 是当前的帧,<code>f-&gt;f_globals</code> 是它的全局区域</li> <li>如果我们没有找到 <code>op_target</code>,我们将会检查这个异常是不是 <code>KeyError</code></li> <li><code>goto error;</code>是一种在 main loop 中抛出异常的方法</li> <li><code>PyErr_Clear()</code>抑制了当前异常的抛出,而<code>DISPATCH()</code>触发了下一个操作码的执行</li> </ul> <p>下一步就是收集我们想要的堆栈信息。</p> <div class="highlight"><pre><code class="language-C" data-lang="C"><span class="cm">/** This code create a list with all the values on the current stack **/</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">value</span> <span class="o">=</span> <span class="n">PyList_New</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">1</span> <span class="p">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="n">STACK_LEVEL</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">tmp</span> <span class="o">=</span> <span class="n">PEEK</span><span class="p">(</span><span class="n">i</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">tmp</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="n">tmp</span> <span class="o">=</span> <span class="n">Py_None</span><span class="p">;</span> <span class="p">}</span> <span class="n">PyList_Append</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">tmp</span><span class="p">);</span> <span class="p">}</span> </code></pre></div> <p>最后一步就是调用我们的回调函数!我们用 <code>call_function</code>来搞定这件事,我们通过研究操作码<code>CALL_FUNCTION</code>的实现来学习怎么使用<code>call_function</code></p> <div class="highlight"><pre><code class="language-C" data-lang="C"><span class="cm">/** From: Python/ceval.c **/</span> <span class="n">TARGET</span><span class="p">(</span><span class="n">CALL_FUNCTION</span><span class="p">)</span> <span class="p">{</span> <span class="n">PyObject</span> <span class="o">**</span><span class="n">sp</span><span class="p">,</span> <span class="o">*</span><span class="n">res</span><span class="p">;</span> <span class="cm">/** stack_pointer is a local of the main loop.</span> <span class="cm"> It&#39;s the pointer to the stacktop of our frame **/</span> <span class="n">sp</span> <span class="o">=</span> <span class="n">stack_pointer</span><span class="p">;</span> <span class="n">res</span> <span class="o">=</span> <span class="n">call_function</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sp</span><span class="p">,</span> <span class="n">oparg</span><span class="p">);</span> <span class="cm">/** call_function handles the args it consummed on the stack for us **/</span> <span class="n">stack_pointer</span> <span class="o">=</span> <span class="n">sp</span><span class="p">;</span> <span class="n">PUSH</span><span class="p">(</span><span class="n">res</span><span class="p">);</span> <span class="cm">/** Standard exception handling **/</span> <span class="k">if</span> <span class="p">(</span><span class="n">res</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="k">goto</span> <span class="n">error</span><span class="p">;</span> <span class="n">DISPATCH</span><span class="p">();</span> <span class="p">}</span> </code></pre></div> <p>有了上面这些信息,我们终于可以捣鼓出一个操作码<code>DEBUG_OP</code>的草稿了</p> <div class="highlight"><pre><code class="language-C" data-lang="C"><span class="n">TARGET</span><span class="p">(</span><span class="n">DEBUG_OP</span><span class="p">)</span> <span class="p">{</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">value</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">target</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">res</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="n">PyObject</span> <span class="o">**</span><span class="n">sp</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="n">PyObject</span> <span class="o">*</span><span class="n">tmp</span><span class="p">;</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="n">_Py_IDENTIFIER</span><span class="p">(</span><span class="n">op_target</span><span class="p">);</span> <span class="n">target</span> <span class="o">=</span> <span class="n">_PyDict_GetItemId</span><span class="p">(</span><span class="n">f</span><span class="o">-&gt;</span><span class="n">f_globals</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">PyId_op_target</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">target</span> <span class="o">==</span> <span class="nb">NULL</span> <span class="o">&amp;&amp;</span> <span class="n">_PyErr_OCCURRED</span><span class="p">())</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">PyErr_ExceptionMatches</span><span class="p">(</span><span class="n">PyExc_KeyError</span><span class="p">))</span> <span class="k">goto</span> <span class="n">error</span><span class="p">;</span> <span class="n">PyErr_Clear</span><span class="p">();</span> <span class="n">DISPATCH</span><span class="p">();</span> <span class="p">}</span> <span class="n">value</span> <span class="o">=</span> <span class="n">PyList_New</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="n">Py_INCREF</span><span class="p">(</span><span class="n">target</span><span class="p">);</span> <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">1</span> <span class="p">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="n">STACK_LEVEL</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">tmp</span> <span class="o">=</span> <span class="n">PEEK</span><span class="p">(</span><span class="n">i</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">tmp</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="n">tmp</span> <span class="o">=</span> <span class="n">Py_None</span><span class="p">;</span> <span class="n">PyList_Append</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">tmp</span><span class="p">);</span> <span class="p">}</span> <span class="n">PUSH</span><span class="p">(</span><span class="n">target</span><span class="p">);</span> <span class="n">PUSH</span><span class="p">(</span><span class="n">value</span><span class="p">);</span> <span class="n">Py_INCREF</span><span class="p">(</span><span class="n">f</span><span class="p">);</span> <span class="n">PUSH</span><span class="p">(</span><span class="n">f</span><span class="p">);</span> <span class="n">sp</span> <span class="o">=</span> <span class="n">stack_pointer</span><span class="p">;</span> <span class="n">res</span> <span class="o">=</span> <span class="n">call_function</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sp</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span> <span class="n">stack_pointer</span> <span class="o">=</span> <span class="n">sp</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">res</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="k">goto</span> <span class="n">error</span><span class="p">;</span> <span class="n">Py_DECREF</span><span class="p">(</span><span class="n">res</span><span class="p">);</span> <span class="n">DISPATCH</span><span class="p">();</span> <span class="p">}</span> </code></pre></div> <p>在编写 CPython 实现的 C 代码方面我确实没有什么经验,有可能我漏掉了些细节。如果您有什么建议还请您纠正,我期待您的反馈。</p> <p>编译它,成了!</p> <p>一切看起来很顺利但是当我们尝试去使用我们定义的操作码<code>DEBUG_OP</code>的时候却失败了。自从 2008 年之后,Python 使用预先写好的<a href="http://bugs.python.org/issue4753">goto</a>(你也可以从<a href="http://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables">这里</a>获取更多的讯息)。故,我们需要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改</p> <div class="highlight"><pre><code class="language-C" data-lang="C"><span class="cm">/** From: Python/opcode_targets.h **/</span> <span class="cm">/** Easy change since DEBUG_OP is the opcode number 1 **/</span> <span class="k">static</span> <span class="kt">void</span> <span class="o">*</span><span class="n">opcode_targets</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">//&amp;&amp;_unknown_opcode,</span> <span class="o">&amp;&amp;</span><span class="n">TARGET_DEBUG_OP</span><span class="p">,</span> <span class="o">&amp;&amp;</span><span class="n">TARGET_POP_TOP</span><span class="p">,</span> <span class="cm">/** ... **/</span> </code></pre></div> <p>这就完事了,我们现在就有了一个可以工作的新操作码。唯一的问题就是这货虽然存在但是没有被人调用过。接下来,我们将<code>DEBUG_OP</code>注入到函数的字节码中。</p> <h2>在 Python 字节码中注入操作码 DEBUG_OP</h2> <p>有很多方式可以在 Python 字节码中注入新的操作码</p> <ul> <li>使用 peephole optimizer,<a href="http://blog.quarkslab.com/building-an-obfuscated-python-interpreter-we-need-more-opcodes.html">Quarkslab</a>就是这么干的</li> <li>在生成字节码的代码中动些手脚</li> <li>在运行时直接修改函数的字节码(这就是我们将要干的事儿)</li> </ul> <p>为了创造出一个新操作码,有了上面的那一堆 C 代码就够了。现在让我们回到原点,开始理解奇怪甚至神奇的 Python!</p> <p>我们将要做的事儿有</p> <ul> <li>得到我们想要追踪函数的 code object</li> <li>重写字节码来注入<code>DEBUG_OP</code></li> <li>将新生成的 code object 替换回去</li> </ul> <h3>和 code object 有关的小贴士</h3> <p>如果你从没听说过 code object,这里有一个简单的<a href="http://blog.hakril.net/articles/0-understanding-python-by-breaking-it.html">介绍</a> 网路上也有一些相关的<a href="https://docs.python.org/3.4/reference/datamodel.html">文档</a>可供查阅,可以直接 <code>Ctrl+F</code> 查找 code object</p> <p>还有一件事情需要注意的是在这篇文章所指的环境中 code object 是<strong>不可变</strong>的</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Python 3.4.2 (default, Oct 8 2014, 10:45:20) [GCC 4.9.1] on linux Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information. &gt;&gt;&gt; x = lambda y : 2 &gt;&gt;&gt; x.__code__ &lt;code object &lt;lambda&gt; at 0x7f481fd88390, file &quot;&lt;stdin&gt;&quot;, line 1&gt; &gt;&gt;&gt; x.__code__.co_name &#39;&lt;lambda&gt;&#39; &gt;&gt;&gt; x.__code__.co_name = &#39;truc&#39; Traceback (most recent call last): File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt; AttributeError: readonly attribute &gt;&gt;&gt; x.__code__.co_consts = (&#39;truc&#39;,) Traceback (most recent call last): File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt; AttributeError: readonly attribute </code></pre></div> <p>但是不用担心,我们将会找到方法绕过这个问题的</p> <h2>使用的工具</h2> <p>为了修改字节码我们需要一些工具</p> <ul> <li><a href="https://docs.python.org/3.4/library/dis.html">dis</a> 模块用来反编译和分析字节码</li> <li><code>dis.Bytecode</code>Python 3.4 新增的一个特性,对于反编译和分析字节码特别有用</li> <li>一个能够简单修改 code object 的方法</li> </ul> <p>用<code>dis.Bytecode</code>反编译 code bject 能告诉我们一些有关操作码、参数和上下文的信息</p> <div class="highlight"><pre><code class="language-text" data-lang="text"># Python3.4 &gt;&gt;&gt; import dis &gt;&gt;&gt; f = lambda x: x + 3 &gt;&gt;&gt; for i in dis.Bytecode(f.__code__): print (i) ... Instruction(opname=&#39;LOAD_FAST&#39;, opcode=124, arg=0, argval=&#39;x&#39;, argrepr=&#39;x&#39;, offset=0, starts_line=1, is_jump_target=False) Instruction(opname=&#39;LOAD_CONST&#39;, opcode=100, arg=1, argval=3, argrepr=&#39;3&#39;, offset=3, starts_line=None, is_jump_target=False) Instruction(opname=&#39;BINARY_ADD&#39;, opcode=23, arg=None, argval=None, argrepr=&#39;&#39;, offset=6, starts_line=None, is_jump_target=False) Instruction(opname=&#39;RETURN_VALUE&#39;, opcode=83, arg=None, argval=None, argrepr=&#39;&#39;, offset=7, starts_line=None, is_jump_target=False) </code></pre></div> <p>为了能够修改 code object 我定义了一个很小的类用来复制 code object 同时能够按我们的需求修改相应的值,然后重新生成一个新的 code object</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">MutableCodeObject</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="n">args_name</span> <span class="o">=</span> <span class="p">(</span><span class="s">&quot;co_argcount&quot;</span><span class="p">,</span> <span class="s">&quot;co_kwonlyargcount&quot;</span><span class="p">,</span> <span class="s">&quot;co_nlocals&quot;</span><span class="p">,</span> <span class="s">&quot;co_stacksize&quot;</span><span class="p">,</span> <span class="s">&quot;co_flags&quot;</span><span class="p">,</span> <span class="s">&quot;co_code&quot;</span><span class="p">,</span> <span class="s">&quot;co_consts&quot;</span><span class="p">,</span> <span class="s">&quot;co_names&quot;</span><span class="p">,</span> <span class="s">&quot;co_varnames&quot;</span><span class="p">,</span> <span class="s">&quot;co_filename&quot;</span><span class="p">,</span> <span class="s">&quot;co_name&quot;</span><span class="p">,</span> <span class="s">&quot;co_firstlineno&quot;</span><span class="p">,</span> <span class="s">&quot;co_lnotab&quot;</span><span class="p">,</span> <span class="s">&quot;co_freevars&quot;</span><span class="p">,</span> <span class="s">&quot;co_cellvars&quot;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">initial_code</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">initial_code</span> <span class="o">=</span> <span class="n">initial_code</span> <span class="k">for</span> <span class="n">attr_name</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">args_name</span><span class="p">:</span> <span class="n">attr</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">initial_code</span><span class="p">,</span> <span class="n">attr_name</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">attr</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">):</span> <span class="n">attr</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">attr</span><span class="p">)</span> <span class="nb">setattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">attr_name</span><span class="p">,</span> <span class="n">attr</span><span class="p">)</span> <span class="k">def</span> <span class="nf">get_code</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">args</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">for</span> <span class="n">attr_name</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">args_name</span><span class="p">:</span> <span class="n">attr</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">attr_name</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">attr</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span> <span class="n">attr</span> <span class="o">=</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">attr</span><span class="p">)</span> <span class="n">args</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">attr</span><span class="p">)</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">initial_code</span><span class="o">.</span><span class="n">__class__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">)</span> </code></pre></div> <p>这个类用起来很方便,解决了上面提到的 code object 不可变的问题</p> <div class="highlight"><pre><code class="language-text" data-lang="text">&gt;&gt;&gt; x = lambda y : 2 &gt;&gt;&gt; m = MutableCodeObject(x.__code__) &gt;&gt;&gt; m &lt;new_code.MutableCodeObject object at 0x7f3f0ea546a0&gt; &gt;&gt;&gt; m.co_consts [None, 2] &gt;&gt;&gt; m.co_consts[1] = &#39;3&#39; &gt;&gt;&gt; m.co_name = &#39;truc&#39; &gt;&gt;&gt; m.get_code() &lt;code object truc at 0x7f3f0ea2bc90, file &quot;&lt;stdin&gt;&quot;, line 1&gt; </code></pre></div> <h2>测试我们的新操作码</h2> <p>我们现在拥有了注入<code>DEBUG_OP</code>的所有工具,让我们来验证下我们的实现是否可用。</p> <p>我们将我们的操作码注入到一个最简单的函数中</p> <div class="highlight"><pre><code class="language-text" data-lang="text">from new_code import MutableCodeObject def op_target(*args): print(&quot;WOOT&quot;) print(&quot;op_target called with args &lt;{0}&gt;&quot;.format(args)) def nop(): pass new_nop_code = MutableCodeObject(nop.__code__) new_nop_code.co_code = b&quot;\x00&quot; + new_nop_code.co_code[0:3] + b&quot;\x00&quot; + new_nop_code.co_code[-1:] new_nop_code.co_stacksize += 3 nop.__code__ = new_nop_code.get_code() import dis dis.dis(nop) nop() # Don&#39;t forget that ./python is our custom Python implementing DEBUG_OP hakril@computer ~/python/CPython3.5 % ./python proof.py 8 0 &lt;0&gt; 1 LOAD_CONST 0 (None) 4 &lt;0&gt; 5 RETURN_VALUE WOOT op_target called with args &lt;([], &lt;frame object at 0x7fde9eaebdb0&gt;)&gt; WOOT op_target called with args &lt;([None], &lt;frame object at 0x7fde9eaebdb0&gt;)&gt; </code></pre></div> <p>看起来它成功了!有一行代码需要说明一下<code>new_nop_code.co_stacksize += 3</code></p> <ul> <li>co_stacksize 表示 code object 所需要的堆栈的大小</li> <li>操作码<code>DEBUG_OP</code>往堆栈中增加了三项,所以我们需要为这些增加的项预留些空间</li> </ul> <p>现在我们可以将我们的操作码注入到每一个 Python 函数中了!</p> <h2>重写字节码</h2> <p>正如我们在上面的例子中所看到的那样,重写 Pyhton 的字节码似乎 so easy。为了在每一个操作码之间注入我们的操作码,我们需要获取每一个操作码的偏移量,然后将我们的操作码注入到这些位置上(把我们操作码注入到参数上是有坏处大大滴)。这些偏移量也很容易获取,使用<code>dis.Bytecode</code></p> <p>就像这样</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">add_debug_op_everywhere</span><span class="p">(</span><span class="n">code_obj</span><span class="p">):</span> <span class="c"># We get every instruction offset in the code object</span> <span class="n">offsets</span> <span class="o">=</span> <span class="p">[</span><span class="n">instr</span><span class="o">.</span><span class="n">offset</span> <span class="k">for</span> <span class="n">instr</span> <span class="ow">in</span> <span class="n">dis</span><span class="o">.</span><span class="n">Bytecode</span><span class="p">(</span><span class="n">code_obj</span><span class="p">)]</span> <span class="c"># And insert a DEBUG_OP at every offset</span> <span class="k">return</span> <span class="n">insert_op_debug_list</span><span class="p">(</span><span class="n">code_obj</span><span class="p">,</span> <span class="n">offsets</span><span class="p">)</span> <span class="k">def</span> <span class="nf">insert_op_debug_list</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">offsets</span><span class="p">):</span> <span class="c"># We insert the DEBUG_OP one by one</span> <span class="k">for</span> <span class="n">nb</span><span class="p">,</span> <span class="n">off</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="nb">sorted</span><span class="p">(</span><span class="n">offsets</span><span class="p">)):</span> <span class="c"># Need to ajust the offsets by the number of opcodes already inserted before</span> <span class="c"># That&#39;s why we sort our offsets!</span> <span class="n">code</span> <span class="o">=</span> <span class="n">insert_op_debug</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">off</span> <span class="o">+</span> <span class="n">nb</span><span class="p">)</span> <span class="k">return</span> <span class="n">code</span> <span class="c"># Last problem: what does insert_op_debug looks like?</span> </code></pre></div> <p>基于上面的例子,有人可能会想我们的<code>insert_op_debug</code>会在指定的偏移量增加一个<code>&quot;\x00&quot;</code>,这尼玛是个坑啊!我们第一个<code>DEBUG_OP</code>注入的例子中被注入的函数是没有任何的分支的,为了能够实现完美一个函数注入函数<code>insert_op_debug</code>我们需要考虑到存在分支操作码的情况。</p> <p>Python 的分支一共有两种</p> <ul> <li>绝对分支:看起来是类似这样子的<code>Instruction_Pointer = argument(instruction)</code></li> <li>相对分支:看起来是类似这样子的<code>Instruction_Pointer += argument(instruction)</code> <ul> <li>相对分支总是向前的</li> </ul></li> </ul> <p>我们希望这些分支在我们插入操作码之后仍然能够正常工作,为此我们需要修改一些指令参数。以下是其逻辑流程</p> <ul> <li>对于每一个在插入偏移量之前的相对分支而言 <ul> <li>如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1</li> <li>如果相等,则不需要增加 1 就能够在跳转操作和目标地址之间执行我们的操作码<code>DEBUG_OP</code></li> <li>如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离</li> </ul></li> <li>对于 code object 中的每一个绝对分支而言 <ul> <li>如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1<br></li> <li>如果相等,那么不需要任何修改,理由和相对分支部分是一样的</li> <li>如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离</li> </ul></li> </ul> <p>下面是实现</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="c"># Helper</span> <span class="k">def</span> <span class="nf">bytecode_to_string</span><span class="p">(</span><span class="n">bytecode</span><span class="p">):</span> <span class="k">if</span> <span class="n">bytecode</span><span class="o">.</span><span class="n">arg</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="k">return</span> <span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">&quot;&lt;Bh&quot;</span><span class="p">,</span> <span class="n">bytecode</span><span class="o">.</span><span class="n">opcode</span><span class="p">,</span> <span class="n">bytecode</span><span class="o">.</span><span class="n">arg</span><span class="p">)</span> <span class="k">return</span> <span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">&quot;&lt;B&quot;</span><span class="p">,</span> <span class="n">bytecode</span><span class="o">.</span><span class="n">opcode</span><span class="p">)</span> <span class="c"># Dummy class for bytecode_to_string</span> <span class="k">class</span> <span class="nc">DummyInstr</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">opcode</span><span class="p">,</span> <span class="n">arg</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">opcode</span> <span class="o">=</span> <span class="n">opcode</span> <span class="bp">self</span><span class="o">.</span><span class="n">arg</span> <span class="o">=</span> <span class="n">arg</span> <span class="k">def</span> <span class="nf">insert_op_debug</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">offset</span><span class="p">):</span> <span class="n">opcode_jump_rel</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;FOR_ITER&#39;</span><span class="p">,</span> <span class="s">&#39;JUMP_FORWARD&#39;</span><span class="p">,</span> <span class="s">&#39;SETUP_LOOP&#39;</span><span class="p">,</span> <span class="s">&#39;SETUP_WITH&#39;</span><span class="p">,</span> <span class="s">&#39;SETUP_EXCEPT&#39;</span><span class="p">,</span> <span class="s">&#39;SETUP_FINALLY&#39;</span><span class="p">]</span> <span class="n">opcode_jump_abs</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;POP_JUMP_IF_TRUE&#39;</span><span class="p">,</span> <span class="s">&#39;POP_JUMP_IF_FALSE&#39;</span><span class="p">,</span> <span class="s">&#39;JUMP_ABSOLUTE&#39;</span><span class="p">]</span> <span class="n">res_codestring</span> <span class="o">=</span> <span class="n">b</span><span class="s">&quot;&quot;</span> <span class="n">inserted</span> <span class="o">=</span> <span class="bp">False</span> <span class="k">for</span> <span class="n">instr</span> <span class="ow">in</span> <span class="n">dis</span><span class="o">.</span><span class="n">Bytecode</span><span class="p">(</span><span class="n">code</span><span class="p">):</span> <span class="k">if</span> <span class="n">instr</span><span class="o">.</span><span class="n">offset</span> <span class="o">==</span> <span class="n">offset</span><span class="p">:</span> <span class="n">res_codestring</span> <span class="o">+=</span> <span class="n">b</span><span class="s">&quot;</span><span class="se">\x00</span><span class="s">&quot;</span> <span class="n">inserted</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">if</span> <span class="n">instr</span><span class="o">.</span><span class="n">opname</span> <span class="ow">in</span> <span class="n">opcode_jump_rel</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">inserted</span><span class="p">:</span> <span class="c">#relative jump are always forward</span> <span class="k">if</span> <span class="n">offset</span> <span class="o">&lt;</span> <span class="n">instr</span><span class="o">.</span><span class="n">offset</span> <span class="o">+</span> <span class="mi">3</span> <span class="o">+</span> <span class="n">instr</span><span class="o">.</span><span class="n">arg</span><span class="p">:</span> <span class="c"># inserted beetwen jump and dest: add 1 to dest (3 for size)</span> <span class="c">#If equal: jump on DEBUG_OP to get info before exec instr</span> <span class="n">res_codestring</span> <span class="o">+=</span> <span class="n">bytecode_to_string</span><span class="p">(</span><span class="n">DummyInstr</span><span class="p">(</span><span class="n">instr</span><span class="o">.</span><span class="n">opcode</span><span class="p">,</span> <span class="n">instr</span><span class="o">.</span><span class="n">arg</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span> <span class="k">continue</span> <span class="k">if</span> <span class="n">instr</span><span class="o">.</span><span class="n">opname</span> <span class="ow">in</span> <span class="n">opcode_jump_abs</span><span class="p">:</span> <span class="k">if</span> <span class="n">instr</span><span class="o">.</span><span class="n">arg</span> <span class="o">&gt;</span> <span class="n">offset</span><span class="p">:</span> <span class="n">res_codestring</span> <span class="o">+=</span> <span class="n">bytecode_to_string</span><span class="p">(</span><span class="n">DummyInstr</span><span class="p">(</span><span class="n">instr</span><span class="o">.</span><span class="n">opcode</span><span class="p">,</span> <span class="n">instr</span><span class="o">.</span><span class="n">arg</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span> <span class="k">continue</span> <span class="n">res_codestring</span> <span class="o">+=</span> <span class="n">bytecode_to_string</span><span class="p">(</span><span class="n">instr</span><span class="p">)</span> <span class="c"># replace_bytecode just replaces the original code co_code</span> <span class="k">return</span> <span class="n">replace_bytecode</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">res_codestring</span><span class="p">)</span> </code></pre></div> <p>让我们看一下效果如何</p> <div class="highlight"><pre><code class="language-text" data-lang="text">&gt;&gt;&gt; def lol(x): ... for i in range(10): ... if x == i: ... break &gt;&gt;&gt; dis.dis(lol) 101 0 SETUP_LOOP 36 (to 39) 3 LOAD_GLOBAL 0 (range) 6 LOAD_CONST 1 (10) 9 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 12 GET_ITER &gt;&gt; 13 FOR_ITER 22 (to 38) 16 STORE_FAST 1 (i) 102 19 LOAD_FAST 0 (x) 22 LOAD_FAST 1 (i) 25 COMPARE_OP 2 (==) 28 POP_JUMP_IF_FALSE 13 103 31 BREAK_LOOP 32 JUMP_ABSOLUTE 13 35 JUMP_ABSOLUTE 13 &gt;&gt; 38 POP_BLOCK &gt;&gt; 39 LOAD_CONST 0 (None) 42 RETURN_VALUE &gt;&gt;&gt; lol.__code__ = transform_code(lol.__code__, add_debug_op_everywhere, add_stacksize=3) &gt;&gt;&gt; dis.dis(lol) 101 0 &lt;0&gt; 1 SETUP_LOOP 50 (to 54) 4 &lt;0&gt; 5 LOAD_GLOBAL 0 (range) 8 &lt;0&gt; 9 LOAD_CONST 1 (10) 12 &lt;0&gt; 13 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 16 &lt;0&gt; 17 GET_ITER &gt;&gt; 18 &lt;0&gt; 102 19 FOR_ITER 30 (to 52) 22 &lt;0&gt; 23 STORE_FAST 1 (i) 26 &lt;0&gt; 27 LOAD_FAST 0 (x) 30 &lt;0&gt; 103 31 LOAD_FAST 1 (i) 34 &lt;0&gt; 35 COMPARE_OP 2 (==) 38 &lt;0&gt; 39 POP_JUMP_IF_FALSE 18 42 &lt;0&gt; 43 BREAK_LOOP 44 &lt;0&gt; 45 JUMP_ABSOLUTE 18 48 &lt;0&gt; 49 JUMP_ABSOLUTE 18 &gt;&gt; 52 &lt;0&gt; 53 POP_BLOCK &gt;&gt; 54 &lt;0&gt; 55 LOAD_CONST 0 (None) 58 &lt;0&gt; 59 RETURN_VALUE # Setup the simplest handler EVER &gt;&gt;&gt; def op_target(stack, frame): ... print (stack) # GO &gt;&gt;&gt; lol(2) [] [] [&lt;class &#39;range&#39;&gt;] [10, &lt;class &#39;range&#39;&gt;] [range(0, 10)] [&lt;range_iterator object at 0x7f1349afab80&gt;] [0, &lt;range_iterator object at 0x7f1349afab80&gt;] [&lt;range_iterator object at 0x7f1349afab80&gt;] [2, &lt;range_iterator object at 0x7f1349afab80&gt;] [0, 2, &lt;range_iterator object at 0x7f1349afab80&gt;] [False, &lt;range_iterator object at 0x7f1349afab80&gt;] [&lt;range_iterator object at 0x7f1349afab80&gt;] [1, &lt;range_iterator object at 0x7f1349afab80&gt;] [&lt;range_iterator object at 0x7f1349afab80&gt;] [2, &lt;range_iterator object at 0x7f1349afab80&gt;] [1, 2, &lt;range_iterator object at 0x7f1349afab80&gt;] [False, &lt;range_iterator object at 0x7f1349afab80&gt;] [&lt;range_iterator object at 0x7f1349afab80&gt;] [2, &lt;range_iterator object at 0x7f1349afab80&gt;] [&lt;range_iterator object at 0x7f1349afab80&gt;] [2, &lt;range_iterator object at 0x7f1349afab80&gt;] [2, 2, &lt;range_iterator object at 0x7f1349afab80&gt;] [True, &lt;range_iterator object at 0x7f1349afab80&gt;] [&lt;range_iterator object at 0x7f1349afab80&gt;] [] [None] </code></pre></div> <p>甚好!现在我们知道了如何获取堆栈信息和 Python 中每一个操作对应的帧信息。上面结果所展示的结果目前而言并不是很实用。在最后一部分中让我们对注入做进一步的封装。</p> <h2>增加 Python 封装</h2> <p>正如您所见到的,所有的底层接口都是好用的。我们最后要做的一件事是让 op_target 更加方便使用(这部分相对而言比较空泛一些,毕竟在我看来这不是整个项目中最有趣的部分)</p> <p>首先我们来看一下帧的参数所能提供的信息,如下所示</p> <ul> <li><code>f_code</code>当前帧将执行的 code object</li> <li><code>f_lasti</code>当前的操作(code object 中的字节码字符串的索引)</li> </ul> <p>经过我们的处理我们可以得知<code>DEBUG_OP</code>之后要被执行的操作码,这对我们聚合数据并展示是相当有用的。</p> <p>新建一个用于追踪函数内部机制的类</p> <ul> <li>改变函数自身的 <code>co_code</code></li> <li>设置回调函数作为<code>op_debug</code>的目标函数</li> </ul> <p>一旦我们知道下一个操作,我们就可以分析它并修改它的参数。举例来说我们可以增加一个<code>auto-follow-called-functions</code>的特性</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">op_target</span><span class="p">(</span><span class="n">l</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="n">exc</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span> <span class="k">if</span> <span class="n">op_target</span><span class="o">.</span><span class="n">callback</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">op_target</span><span class="o">.</span><span class="n">callback</span><span class="p">(</span><span class="n">l</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="n">exc</span><span class="p">)</span> <span class="k">class</span> <span class="nc">Trace</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">func</span> <span class="o">=</span> <span class="n">func</span> <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">add_func_to_trace</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">func</span><span class="p">)</span> <span class="c"># Activate Trace callback for the func call</span> <span class="n">op_target</span><span class="o">.</span><span class="n">callback</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">callback</span> <span class="k">try</span><span class="p">:</span> <span class="n">res</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="n">res</span> <span class="o">=</span> <span class="n">e</span> <span class="n">op_target</span><span class="o">.</span><span class="n">callback</span> <span class="o">=</span> <span class="bp">None</span> <span class="k">return</span> <span class="n">res</span> <span class="k">def</span> <span class="nf">add_func_to_trace</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">f</span><span class="p">):</span> <span class="c"># Is it code? is it already transformed?</span> <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">f</span> <span class="p">,</span><span class="s">&quot;op_debug&quot;</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">&quot;__code__&quot;</span><span class="p">):</span> <span class="n">f</span><span class="o">.</span><span class="n">__code__</span> <span class="o">=</span> <span class="n">transform_code</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">__code__</span><span class="p">,</span> <span class="n">transform</span><span class="o">=</span><span class="n">add_everywhere</span><span class="p">,</span> <span class="n">add_stacksize</span><span class="o">=</span><span class="n">ADD_STACK</span><span class="p">)</span> <span class="n">f</span><span class="o">.</span><span class="n">__globals__</span><span class="p">[</span><span class="s">&#39;op_target&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">op_target</span> <span class="n">f</span><span class="o">.</span><span class="n">op_debug</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">def</span> <span class="nf">do_auto_follow</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">stack</span><span class="p">,</span> <span class="n">frame</span><span class="p">):</span> <span class="c"># Nothing fancy: FrameAnalyser is just the wrapper that gives the next executed instruction</span> <span class="n">next_instr</span> <span class="o">=</span> <span class="n">FrameAnalyser</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span><span class="o">.</span><span class="n">next_instr</span><span class="p">()</span> <span class="k">if</span> <span class="s">&quot;CALL&quot;</span> <span class="ow">in</span> <span class="n">next_instr</span><span class="o">.</span><span class="n">opname</span><span class="p">:</span> <span class="n">arg</span> <span class="o">=</span> <span class="n">next_instr</span><span class="o">.</span><span class="n">arg</span> <span class="n">f_index</span> <span class="o">=</span> <span class="p">(</span><span class="n">arg</span> <span class="o">&amp;</span> <span class="mh">0xff</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="p">(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">))</span> <span class="n">called_func</span> <span class="o">=</span> <span class="n">stack</span><span class="p">[</span><span class="n">f_index</span><span class="p">]</span> <span class="c"># If call target is not traced yet: do it</span> <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">called_func</span><span class="p">,</span> <span class="s">&quot;op_debug&quot;</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">add_func_to_trace</span><span class="p">(</span><span class="n">called_func</span><span class="p">)</span> </code></pre></div> <p>现在我们实现一个 Trace 的子类,在这个子类中增加 callback 和 do<em>report 这两个方法。callback 方法将在每一个操作之后被调用。do</em>report 方法将我们收集到的信息打印出来。</p> <p>这是一个伪函数追踪器实现</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">DummyTrace</span><span class="p">(</span><span class="n">Trace</span><span class="p">):</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">func</span> <span class="o">=</span> <span class="n">func</span> <span class="bp">self</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">collections</span><span class="o">.</span><span class="n">OrderedDict</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">last_frame</span> <span class="o">=</span> <span class="bp">None</span> <span class="bp">self</span><span class="o">.</span><span class="n">known_frame</span> <span class="o">=</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">report</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">def</span> <span class="nf">callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">stack</span><span class="p">,</span> <span class="n">frame</span><span class="p">,</span> <span class="n">exc</span><span class="p">):</span> <span class="k">if</span> <span class="n">frame</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">known_frame</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">known_frame</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">report</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s">&quot; === Entering New Frame {0} ({1}) ===&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">frame</span><span class="o">.</span><span class="n">f_code</span><span class="o">.</span><span class="n">co_name</span><span class="p">,</span> <span class="nb">id</span><span class="p">(</span><span class="n">frame</span><span class="p">)))</span> <span class="bp">self</span><span class="o">.</span><span class="n">last_frame</span> <span class="o">=</span> <span class="n">frame</span> <span class="k">if</span> <span class="n">frame</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">last_frame</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">report</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s">&quot; === Returning to Frame {0} {1}===&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">frame</span><span class="o">.</span><span class="n">f_code</span><span class="o">.</span><span class="n">co_name</span><span class="p">,</span> <span class="nb">id</span><span class="p">(</span><span class="n">frame</span><span class="p">)))</span> <span class="bp">self</span><span class="o">.</span><span class="n">last_frame</span> <span class="o">=</span> <span class="n">frame</span> <span class="bp">self</span><span class="o">.</span><span class="n">report</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">stack</span><span class="p">))</span> <span class="n">instr</span> <span class="o">=</span> <span class="n">FrameAnalyser</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span><span class="o">.</span><span class="n">next_instr</span><span class="p">()</span> <span class="n">offset</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">instr</span><span class="o">.</span><span class="n">offset</span><span class="p">)</span><span class="o">.</span><span class="n">rjust</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="n">opname</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">instr</span><span class="o">.</span><span class="n">opname</span><span class="p">)</span><span class="o">.</span><span class="n">ljust</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span> <span class="n">arg</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">instr</span><span class="o">.</span><span class="n">arg</span><span class="p">)</span><span class="o">.</span><span class="n">ljust</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">report</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s">&quot;{0} {1} {2} {3}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="n">opname</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="n">instr</span><span class="o">.</span><span class="n">argval</span><span class="p">))</span> <span class="bp">self</span><span class="o">.</span><span class="n">do_auto_follow</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">frame</span><span class="p">)</span> <span class="k">def</span> <span class="nf">do_report</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;</span><span class="se">\n</span><span class="s">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">report</span><span class="p">))</span> </code></pre></div> <p>这里有一些实现的例子和使用方法。格式有些不方便观看,毕竟我并不擅长于搞这种对用户友好的报告的事儿</p> <ul> <li><a href="http://hakril.net/python/article/example_dummy_trace.html">例1</a> 自动追踪堆栈信息和已经执行的指令</li> <li><a href="http://hakril.net/python/article/example_dummy_contextmanager.html">例2</a>上下文管理</li> </ul> <p>递推式构造列表(List Comprehensions)的追踪示例</p> <ul> <li><a href="http://hakril.net/python/article/example_list_comp_dummy.html">例3</a>伪追踪器的输出</li> <li><a href="http://hakril.net/python/article/example_list_comp_stacktrace.html">例4</a>输出收集的堆栈信息</li> </ul> <h2>总结</h2> <p>这个小项目是一个了解 Python 底层的良好途径,包括解释器的 main loop,Python 实现的 C 代码编程、Python 字节码。通过这个小工具我们可以看到 Python 一些有趣构造函数的字节码行为,例如生成器、上下文管理和递推式构造列表。</p> <p><a href="http://hakril.net/blog_data/assembly_tracer_code.zip">这里</a>是这个小项目的完整代码。</p> <p>更进一步的,我们还可以做的是修改我们所追踪的函数的堆栈。我虽然不确定这个是否有用,但是可以肯定是这一过程是相当有趣的。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】 沙箱中的间谍 - 可行的 JavaScript 高速缓存区攻击 /nodejs/2015/05/27/nodejs-cache_attack_in_javascript/ Wed, 27 May 2015 00:00:00 +0000 /nodejs/2015/05/27/nodejs-cache_attack_in_javascript <p>原文连接 <a href="http://arxiv.org/pdf/1502.07373v2.pdf">The Spy in the Sandbox – Practical Cache Attacks in Javascript</a></p> <p>相关论文可在 <a href="https://github.com/wyvernnot/cache_attack_in_javascript">https://github.com/wyvernnot/cache_attack_in_javascript</a> 下载</p> <h2>作者和单位</h2> <ul> <li>Yossef Oren (yos AT cs.columbia.edu)</li> <li>Vasileios P. Kemerlis (vpk AT cs.columbia.edu)</li> <li>Simha Sethumadhavan (simha AT cs.columbia.edu)</li> <li>Angelos D. Keromytis (angelos AT cs.columbia.edu)</li> </ul> <p>哥伦比亚大学计算机系</p> <h2>摘要</h2> <p>我们将展示首个完全运行在浏览器里的针对微架构的边信道攻击手段。与这个领域里的其它研究成果不同,这一手段不需要攻击者在受害者的电脑上安装任何的 应用程序来展开攻击,受害者只需要打开一个由攻击者控制的恶意网页。这种攻击模型可伸缩性高,易行,贴近当今的网络环境,特别是由于绝大多数桌面浏览器连接到 Internet 因此几乎无法防御。这种攻击手段基于 Yarom 等人在 文[23] 提出的 LLC 攻击,可以让攻击者远程获得属于其它进程、用户甚至是虚拟机的信息, 只要它们和受害者的浏览器运行在运行在同一台物理主机上。我们将阐述这种攻击背后的基本原理,然后用一种高带宽的隐藏通道验证它的效果,最后 用它打造了一个覆盖整个系统的鼠标和网络活动记录器。抵御这种攻击是可能的,但是所需的反制措施对浏览器和电脑的正常使用所产生的代价有点不实际。</p> <h2>1 引言</h2> <p>边信道分析是里一种非常强大的密码分析攻击。攻击者通过分析安全设备内部在进行安全运算时所产生的的物理信号(功率,辐射,热量等)来取得秘密信息[15]。 据说在二战中便有情报部门在使用,Kocher 等人在1996年首次在学术情境下讨论了这个问题[14]。边信道分析被证实可以用来侵入无数的现实世界中的系统,从汽车报警器 到高安全性的密码协处理器[8][18]。缓存攻击(Cache Attack)是和个人电脑相关的一种边信道攻击,因为高速缓冲区被不同的进程和用户使用而导致了信息的泄露[17][11]。</p> <p>虽然边信道攻击的能力无可置疑,但是要实际应用到系统上还是相对受限的。影响边信道攻击可行性的一个主要因素是对不确定的<strong>攻击模型</strong>的假设:除了基于网络的 时序攻击,大部分的边信道攻击都要求攻击者非常接近受害者。缓存攻击一般会假设攻击者能够在受害者的机器上执行任意的二进制的代码。虽然这个假设 适用于像 Amazon 云计算平台这样的 IaaS 或者 PaaS 环境,但是对于其它环境就不那么贴切了。</p> <p>在这篇报告里,我们用一种约束更少、更可行的攻击者模型挑战了这一限制性的安全假设。在我们的攻击模型里,受害者只需要<strong>访问一个网页</strong>,这个网页 由攻击者所拥有。我们会展示,即使在这么简单的攻击者模型里,攻击者依然能够在可行的时间周期里,从被攻击的系统中提取出有意义的信息。为了和这样的 计算设定保持一致,我们把注意力集中在了<strong>跟踪用户行为</strong>而不是获取密匙上。报告中的攻击方式因此是高度可行的:对于攻击者的假设和限定是 实际的;运行的时间是实际的;给攻击者带来的好处也是实际的。据我们了解,这是首个可以轻松扩展至上百万个目标的边信道攻击方式。</p> <p>我们假设攻击中受害者使用的个人电脑配备有较新型号的的 Intel CPU ,并进一步假设用户用支持 HTML5 的浏览器访问网页。这覆盖了绝大部分连接到 Internet 的 个人电脑,见 章节5.1 。用户被强迫访问一个页面,这个页面上有一个由攻击者控制的元素比如广告。攻击代码自己会执行基于 JavaScript 的 缓存攻击,见 章节2,持续地访问被攻击系统的 LLC 。因为所有的 CPU 内核,用户,进程,保护环等共享同一个高速缓存区,所以可为攻击者提供被攻击用户和系统的详细信息。</p> <h3>1.1 现代 Intel CPU 的内存架构</h3> <p>现代的计算机系统通常会采用一个高速的中央处理器(CPU)和一个容量大但是速度较慢的随机存取器(RAM)。为了克服这两个模块的性能鸿沟,现代的计算机系统会采用 高速缓存 - 一种容量小但是速度更快的内存结构,里面保存了 RAM 中最近被 CPU 访问过的子集。高速缓存通常会采用 <strong>分层</strong> 设计,即在 CPU 和 RAM 之间 <strong>分层</strong> 放置一些列逐渐变大和变慢的内存结构。图1 取自 文[22],展示了 Intel Ivy Bridge 的缓存结构,包括:较小的 <strong>level 1(L1) cache</strong>,大一些的 <strong>level 2 (L2) cache</strong> ,最下方是最大的 <strong>level 3 (L3) cache</strong> 并和 RAM 相连。Intel 目前代号为 Haswell 的新一代 CPU 采用了另一种嵌入式的 DRAM(eDRAM) 设计,所以不在本文讨论范围内。如果 CPU 需要访问的数据当前不在缓存里,会触发一个 <strong>未命中</strong> ,当前缓存里的一项必须被 <strong>淘汰</strong> 来给新元素腾出空间。</p> <p><center><img src="/images/cache_attack_in_javascript/figure1.png" alt="图1"></center></p> <p><center> 图1:Intel Ivy Bridge </center></p> <p>Intel 的缓存微架构是 <strong>嵌套的</strong> - 所有一级缓存里的数据必须同时存在二级和三级缓存里。倒过来,如果某个元素在三级缓存里被淘汰,那它也会立刻被 从二级和一级缓存里移走。需要注意的是 AMD 缓存微架构的设计是非嵌套的,所以本文描述的方法并不能立刻应用到该平台上。</p> <p>本文重点关注第三级缓存,通常也被称为 LLC。由于 LLC 较大,CPU 访问内存的时候如果把整个 LLC 的内容都搜索一遍效率会很低。 为了避免这个问题,LLC 通常被分成不同的 <strong>组</strong>,每一组都对应者内存空间的一个固定的子集。每个组包含若干缓存线。例如,Intel Haswell 系列中的 Core i7-3720QM 处理器拥有 8192 = 2^13 个组,每个组有 12 条 64 = 2^6 字节的缓存线,共同组成 8192 x 12 x 64 = 6 MB 的高速缓存。 CPU 如果要检查一个给定的物理地址是否在三级缓存里,会先计算出组,然后只检查组内的缓存线。结果就是,某个物理地址的缓存未命中,会导致同一个组的 为数不多的缓存线中的一条被淘汰,这个事实会被我们的攻击反复运用。由64比特长度的物理地址到13比特长度的组索引的映射方法已经被 Hund 等人在2013年 通过逆向工程得出来[12]。在表示物理地址的64个比特位里,5到0被忽略,16到6被直接用来作为组索引的低11位,63到17散列后得到组索引的高2位。 LLC 被所有的内核、线程、进程、用户,乃至运行在同一块 CPU 芯片上的虚拟机所共享,而不论特权环或其它类似的保护措施。</p> <p>现代的个人电脑采用了一种 <strong>虚拟内存机制</strong>,在这种机制里,用户进程一般无法直接得到或访问系统的物理内存。取而代之的是,进程会被分配不同的虚拟内存<strong>页</strong>。 如果某个虚拟内存页被当前执行的进程访问,操作系统会在物理内存里动态地分配一个<strong>页框</strong>。CPU 的内存管理单元(MMU)负责把不同进程对虚拟内存地址的访问 映射到物理内存。Intel 处理器的页和页框大小一般是4KB,并且页和页框的是按照<strong>页对齐</strong>的 - 每页的开始地址是页大小的倍数。这意味着,任何虚拟地址 的低12位和对应的虚拟地址是一一对应的,这一事实也在我们的攻击中用到。</p> <h3>1.2 缓存攻击</h3> <p>缓存攻击是针对微架构的攻击手段中一个典型的代表, Aciamez 在他的出色的调查中将这类攻击定义为利用 “信任架构边界下方的底层处理器结构” 从不同的安全系统中 获取秘密信息。缓存攻击基于这样的事实:尽管在上层有诸多像沙箱、虚拟内存、特权环,宿主这样的保障机制,安全和不安全进程通过高速缓存区的共用可以互相影响。 攻击者构造一个“间谍”进程后可以通过被共用的缓存来测量和干扰其它安全进程的内部状态。Hu 在1992年的首次发现[11],在随后的一些研究成果里显示了边信道攻击可被用来获取 AES密匙[17][4],RSA密匙[19],甚至可以允许一台虚拟机侵入宿主上的其它机器。</p> <p>我们的攻击建立在 <strong>填充+探测</strong> 模型的基础上,这个方法由 Osvik 在[17]中首次描述,不过是针对一级缓存的。之后由 Yarom 等人在[23]中扩展到启用了 较大内存页系统的 LLC。我们把这个方法扩展了一下支持更加常见的 4K 的页大小。总的来说,<strong>填充+探测</strong> 有四个步骤。第一步,攻击者建立一个 或多个 <strong>移除集</strong>。移除集是内存中的一系列地址,这些地址被访问的时候会占据受害者进程使用的一条缓存线。第二步,攻击者通过访问移除集<strong>填充</strong>整个组。 这会强制受害者的代码或指令被从组中淘汰,并使组进入一个已知的状态。第三步,攻击者触发或只是等待受害者执行和可能使用组。最后,攻击者通过再次访问移除集 来<strong>探测</strong>组。如果访问的延迟比较低,意味着攻击者的指令或数据还在缓存里。否则,较高的访问延迟意味着受害者的代码用到了组,因此攻击者可以了解受害者的内部状态。 实际的时间测量是用非特权的汇编指令<code>RDTSC</code>进行的,这个指令可以得到处理器非常准确的周期数。再次遍历链表还有第二个目的,那就是强制组进入 受攻击者控制的状态,为下一轮的测量做好准备。</p> <h3>1.3 Web 运行环境</h3> <p>JavaScript 是一种拥有动态类型,基于对象的运行时求值的脚本语言,它支撑着现代互联网的客户端。JavaScript 代码以源码的形式传到浏览器端,由浏览器 <strong>即时编译(JIT)</strong> 机制来编译和优化。不同浏览器厂商之间的激烈竞争使不断改进 JavaScript 性能备受关注。结果就是,在某些场景下,JavaScript 执行的效率已经 可以和机器语言相媲美。</p> <p>JavaScript 语言的核心功能是由 ECMA 产业协会在 ECMA-262 标准中定义的。语言标准由万维网协会(W3C)定义的一系 API 所补充,因此适合开发 Web 内容。 JavaScript API 的集合是不断演进的,浏览器厂商依照自己的开发计划不断增加新的 API 支持。我们的工作中用到两个具体的API: 第一个是类型数组的定义<a href="https://www.khronos.org/registry/typedarray/specs/latest/">9</a>,通过它可以高效地访问非结构化的二进制数组。 第二个是高精度时间API<a href="http://www.w3.org/TR/hr-time/">16</a>,让应用程序可以进行毫秒以下时间的测量。 如 章节5.1 所示,大部分当今主流的浏览器都同时支持这两个API。</p> <p>JavaScript 代码运行在高度<strong>沙箱化</strong>的环境里 - 用 JavaScript 交付的代码对系统的访问非常受限。例如,JavaScript 代码如果没有用户的允许不能打开和读取文件。 JavaScript 代码不能执行机器语言或者加载本地的代码库。 最值得注意的是,JavaScript 代码<strong>没有指针的概念</strong>,所以你连一个 JavaScript 变量虚拟地址都没法知道。</p> <h3>1.4 我们的工作</h3> <p>我们的目的是构造一个可以通过 Web 部署的 LLC 攻击。这个过程是充满挑战的,因为 JavaScript 代码没法加载共享库或者执行本机语言的程序, 并且由于无法直接调用专用的汇编指令而被迫调用脚本语言的函数进行时间的测量。尽管有这些挑战,我们还是成功地把缓存攻击扩展到了基于 Web 环境:</p> <ul> <li><p>我们展示了一种用来在 LLC 上的建立 <strong>非典型移除集</strong> 特别方法。与[23]不同,我们的方法不要求系统配置成支持较大的内存页,所以能够很快的 应用到广泛的桌面和服务器系统。我们展示了该方法虽然是使用 JavaScript 实现的,但是依然可以在实际的时间周期里完成。</p></li> <li><p>我们展示了一种 <strong>功能完善的用无需特权的 JavaScript 发动 LLC 攻击</strong> 的方法。我们用隐藏通道的方式,评估了它的性能,包括在同一个机器、不同进程之间 和在虚拟机与它的主机之间。基于 JavaScript 的通道与[23]中用机器语言实现的方法类似,都可以达到每秒几百 kb 的速度。</p></li> <li><p>我们展示了怎么利用基于缓存的方法来有效地<strong>跟踪用户行为</strong>。缓存攻击的这一应用与我们的攻击模型更相关,这与密码分析在其它成果中的应用不同。</p></li> <li><p>最后,我们分析了针对攻击<strong>可能的反制措施</strong>和整个系统的代价。</p></li> </ul> <p><strong>文档结构</strong>: 第二章,攻击方法不同阶段的设计与实现。 第三章,基于攻击方法建立起来的隐藏通道,这个通道也被用来验证方法的性能。 第四章,缓存攻击如何被用来跟踪用户在浏览器内外的行为。 第五章,总结,提出反制措施和仍未解决的研究挑战。</p> <h2>2 攻击方法</h2> <p>正如前文所诉,一次成功的 填充+探测 攻击包含几个步骤:为一个或多个相关组建立移除集,填充缓存,触发受害者的操作,最后再次探测组。 虽然填充和探测实现起来很简单,但是要找到对应于某个系统操作的组并且为它建立移除集就不那么容易了。在本章里,我们描述了这几个步骤用 JavaScript 如何实现。</p> <h3>2.1 建立一个移除集</h3> <h3>2.1.1 设计</h3> <p>正如[23]写到,<strong>填充+探测</strong>攻击方法的第一步是为某个与被攻击进程共享的组建立一个移除集。这个移除集包含一系列的变量,而且这些变量都被 CPU 映射到相同的 组里。根据 文[20] 的建议,使用链表可以避免 CPU 的内存预读和流水线优化。我们首先展示如何为任意一个组建立一个移除集,然后解决寻找与受害者共享组的问题。</p> <p>文[17]指出,一级缓存是依据虚拟地址的低位的比特来决定组分配的。假设攻击者知道变量的虚拟地址,那么在基于一级缓存的攻击模型里建立移除集很容易。 但是,LLC 里变量的组分配是依照物理内存的地址进行的,而且一般情况下,非特权进程无法知道。文[23]的作者为了规避这个问题,假设系统用的是 页较大的模型,在这个模型里,物理地址和虚拟地址的低21位是相同的,并通过迭代算法来获得组索引的高位。</p> <p>在我们所考虑的攻击模型里,系统运行在 4K 的页大小模型下,物理地址和虚拟地址只有最低的12位是相同的。然而更大的难题是,JavaScript没有指针的概念, 所以即使是自己定义的变量,虚拟地址也是不知道的。</p> <p>从64位物理地址到13位的组索引的映射关系已经被 Hund 等人研究过[12]。他们发现,当访问物理内存里一段连续的、8MB大小的 “淘汰缓冲区” 时会让三级缓存里的所有组 都失效。虽然我们在用户态下没有办法分配这样的一个“淘汰缓冲区” (实际上,文章[12]是通过内核模式的驱动实现的),我们用 JavaScript 在 虚拟内存里分配了一个 8MB 大小的数组(这其实是由系统分配的随机、不连续的 4K 大小物理内存页的集合),然后测量遍历这个缓冲区在全系统造成的影响。 我们发现在迭代访问了这个淘汰缓冲区后如果立即访问内存中其它不相关的变量,访问的延迟会显著的增加。 另外一个发现是,即使不访问整个缓冲区而是每隔64字节去访问它,这个现象依然存在。但是,我们所访问的 131K 个偏移值到8192个可能的组的映射关系 并没有立刻清晰起来,因为我们不知道缓冲区里各个页在物理内存中的地址。</p> <p>解决这个问题一个不太靠谱的做法是,给定一个任意的“受害者”在内存中的地址,通过暴力手段从 131K 个偏移值中找到12个与这个地址共享组的地址。要完成这点, 我们可以从 131K 个偏移量中选取几个作为子集,在迭代了所有的偏移量后再测量下访问的延迟有没有变化。如果延迟增加了,意味着含有12个地址的子集与 受害者地址共享相同的组。如果延迟没有变化,那子集里的12个地址中的任何一个都不在组里,这样受害者地址就还在缓存里。把这个过程重复8192遍,每次用 一个不同的受害者地址,我们就可以识别每个组并且建立自己的数据结构。</p> <p>受此启发而立刻写出来的程序会运行非常长的时间。幸运的是, Intel MMU 的页帧大小(章节1.1)非常有帮助,因为虚拟地址是页对齐的,每个虚拟地址的 低12位和每个物理地址的低12位是一致的。据 Hund 等人所称,12个比特中的6个被用来唯一决定组索引。因此,淘汰缓冲区中的一个偏移会和其它 8K 个偏移 共享12到6位,而不是所有 131K 个。此外,只要找到一个组就能立刻知道其它的63个在相同页帧里的组的位置。再加上 JavaScript 分配大的数据缓存区的时是 和页帧的边界对齐的,所以可以用算法1中的贪心算法。</p> <p><strong>算法1</strong> Profiling a cache set Let S be the set of unmapped pages, and address x be an arbitrary page-aligned address in memory</p> <div class="highlight"><pre><code class="language-text" data-lang="text">1. Repeat k times: (a) Iteratively access all members of S (b) Measure t1 , the time it takes to access x (c) Select a random page s from S and remove it (d) Iteratively access all members of S\s (e) Measure t2 , the time it takes to access x (f) If removing page s caused the memory access to speed up considerably (i.e., t1 − t2 &gt; thres), then this page is part of the same set as x. Place it back into S. (g) If removing page s did not cause memory access to speed up considerably, then this address is not part of the same set as x. 2. If |S| = 12, return S. Otherwise report failure. </code></pre></div> <p>通过多次运行 算法1,我们可以逐渐的建立一个移除集并覆盖大部分的缓存,除了那些被 JavaScript 运行时本身所使用的。我们注意到, 与[23]中的算法建立的淘汰缓冲区不同,我们的移除集是<strong>非典型</strong>的 - 因为 JavaScript 没有指针的概念,所以如果我们发现了一个移除集 我们并没有办法知道它对应着 CPU 高速缓存的哪个组。此外,在相同的机器上每次运行这个算法都会得到不同的映射。这也许是因为用了传统的 4K 页大小 而不是 2MB 的页大小的原因,这个问题即使不用 JavaScript 用机器语言也存在。</p> <h3>2.1.2 验证</h3> <p>我们用 JavaScript 实现了 算法1 并且在安装了 Ivy Bridge, Sandy Bridge,Haswell 系列 CPU 的机器上进行验证,机器上装有 Safari 和 Firefox 对应运行在 Mac OS Yosemite 和 Ubuntu 14.04 LTS 操作系统上。系统并没有被配置使用大的页而是用默认的 4K 页大小。列表1 显示了实现 算法1.d 和 算法1.e 的代码,展示了 JavaScript 下怎么遍历链表和测量时间。算法如果要运行在 Chrome 和 Internet Explorer 下,需要额外的几个步骤,在 章节5.1 中。</p> <p><strong>列表1</strong></p> <div class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="c1">// Invalidate the cache set</span> <span class="kd">var</span> <span class="nx">currentEntry</span> <span class="o">=</span> <span class="nx">startAddress</span><span class="p">;</span> <span class="k">do</span> <span class="p">{</span> <span class="nx">currentEntry</span> <span class="o">=</span> <span class="nx">probeView</span><span class="p">.</span><span class="nx">getUint32</span><span class="p">(</span><span class="nx">currentEntry</span><span class="p">);</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="nx">currentEntry</span> <span class="o">!=</span> <span class="nx">startAddress</span><span class="p">);</span> <span class="c1">// Measure access time</span> <span class="kd">var</span> <span class="nx">startTime</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">performance</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span> <span class="nx">currentEntry</span> <span class="o">=</span> <span class="nx">primeView</span><span class="p">.</span><span class="nx">getUint32</span><span class="p">(</span><span class="nx">variableToAccess</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">endTime</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">performance</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span> </code></pre></div> <p><center><img src="/images/cache_attack_in_javascript/figure2.png" alt="图2"></center></p> <p><center>图2 性能分析算法的累积表现</center></p> <p>图2显示了性能分析的结果,运行在 Intel i7-3720QM CPU 上, 装有 Firefox 35.0.1 和 Mac OS 10.10.2 。我们很高兴地发现在30秒内就 映射了超过25%的组,1分钟内就达到了50%。这个算法想要并行运行是非常简单的,因为大部分的执行时间花在了数据结构的维护上,只有一小部分花在让缓存失效和 测量上。整个算法用不到500行 JavaScript 代码就可以完成。</p> <p><center><img src="/images/cache_attack_in_javascript/figure3.png" alt="图3"></center></p> <p><center>图3 Haswell 上两种方法访问延迟的概率分布</center></p> <p>为了验证我们的算法能够辨别不同的组,我们设计了一个实验来比较一个变量被 flush 前后的访问延迟。图3 显示了两种方式访问变量的概率分布函数。 灰色的代表用我们的方式从缓存中 flush 出去的变量的访问时间;而黑色是驻留在缓存里的变量的访问时间。时间的测量用到 JavaScript 的高精度计时器, 所以还包括了 JavaScript 运行时的延迟。两者的不同是显而易见的。图4 显示的是在较早版本的 Sandy Bridge CPU 上捕捉到的结果,该型号每个组有16个条目。</p> <p>通过选取一些列的组,并且不断的测量它们的访问延迟,攻击者可以获得缓存实时活动非常详细的图。我们把这种视觉呈现称作 “内存谱图”,因为它看起来很像声音的谱图。</p> <p><center><img src="/images/cache_attack_in_javascript/figure4.png" alt="图4"></center></p> <p><center>图4 Sandy Bridge 上两种方法访问延迟的概率分布</center></p> <p><center><img src="/images/cache_attack_in_javascript/figure5.png" alt="图5"></center></p> <p><center>图5 内存谱图示例</center></p> <p>图5显示的是每隔400ms抓取一次的内存谱图。其中X轴对应时间,Y轴对应不同的组。例子中的时间分辨率是250微秒,检测了一共128个组。每个点的密度 代表了这个组在这个时间的访问延迟。黑色代表延迟较低,意味从上次测量到现在没有其它进程访问过这个组。白色意味着攻击者的数据在上次测量之后被淘汰了。</p> <p>细看这个内存谱图可以得到几个显而易见的事实。首先,虽然没用机器语言指令而是用了 JavaScript 的计时器,测量的抖动很小,活跃和不活跃的组 很容易被区分。图中有几条明显的垂直的线,意味着同一时间间隔里有多个相邻的组被访问。因为连续的组对应的物理内存的地址也是连续的,所以我们 相信这个信号代表着一个超过 64 字节的汇编指令。还有一些聚在一起的组被同时访问。我们推断这代表着变量的访问。最后,横着的白线预示着一个变量 被不断地访问。这个变量可能是属于测量代码的或属于当前的 JavaScript 运行时。从一个没有任何特权的网页能得到这么多信息真是太了不起了。</p> <h3>2.2 在缓存里识别意思的区域</h3> <p>移除集让攻击者能够监控任意一个组的活动。因为我们得到的移除集是非典型的,因此攻击者必须想办法把分析过的组和受害者的数据或是代码的地址 关联起来。这个学习/分类的问题已经由 Zhang 和 Yarom 分别在 文章[25] 和 文章[23] 里提出了,他们采用了不同的诸如 SVM 的机器学习的算法试图从 缓存延迟的测量数据里找到规律。</p> <p>为了有效地展开学习过程,攻击者需要诱导受害者做一些操作,然后检查哪些组被这个操作访问到,详见 算法2。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">Let Si be the data structure matched to eviction set i 1. For each set i: (a) Iteratively access all members of Si to prime the cache set (b) Measure the time it takes to iteratively access all members of Si (c) Perform an interesting operation (d) Measure once more the time it takes to iteratively access all members of Si (e) If performing the interesting operation caused the access time to slow down considerably, then the operation was associated with cache set i. </code></pre></div> <p>因为 JavaScript 受到一系列的权限限制,实现步骤(c)是很有挑战的。与之形成对比的是 Apecechea 等人能够用一个空的 <strong>sysenter</strong> 调用来触发一次 细小的内核操作。为了实现这个步骤,我们必须调查 JavaScript 的运行时来发现有哪些函数会触发有意思的行为,例如文件访问,网络访问,内存分配等等。 我们还对那些运行时间相对较短,不会产生遗留的函数感兴趣。因为遗留可能导致垃圾回收,进而影响步骤(d)的测量。Ho 等人在 文章[10] 中已经找到了这样的 几个函数。另外一种方式是诱导用户代替攻击者执行一个特定的操作(比如在键盘上按一个键)。这个例子里的学习过程可能是结构化的(攻击者知道受害者 将要执行的时机),也可能是非结构化的(攻击者只能假设系统一段时间内的响应缓慢是由受害者的操作导致的)。这两种方法都被使用,详见 章节4。</p> <p>因为我们的程序会一直检测到由 JavaScript 运行时产生的活动,比如高性能的计时器的代码,浏览器其它那些与当前执行调用无关的模块的代码,实际上我们 通过调用两个相似的函数并 <strong>对比</strong> 它们两次活动性能分析的结果,以此来寻找相关的组。</p> <h2>3 基于高速缓存区的隐藏信道之 JavaScript 实现</h2> <h3>3.1 动机</h3> <p>正如 文章[23] 所示,LLC 访问模式可被用来建立一个高带宽的隐藏信道,有效的用来在同一台宿主上的两个虚拟机之间渗透敏感的信息。在我们的攻击模型里, 攻击者虽然不在同一台宿主上的虚拟机里,而是在一个网页中,隐藏信道的动机不一样,但是也很有意思。</p> <p>经由动机,我们假设某个安全部门在追踪犯罪大师 Bob 的踪迹。该部门通过钓鱼项目在 Bob 的个人电脑上装了一个被称作 APT( Advanced Persistent Threat ) 的 软件。APT 被设计用来记录 Bob 的犯罪记录并发送到部门的秘密服务器上。然而 Bob 非常的警觉,他使用了启用了强制信息流跟踪 (Information Flow Tracking ) [24] 的操作系统。操作系统的这一功能阻止了 APT 在访问了可能含有用户隐私数据的文件后再连上网络。</p> <p>在这种情况下,只要 Bob 能被诱导访问一个由安全部门控制的网页,这个部门就可以立刻采用基于 JavaScript 的高速缓存区攻击。APT 可以利用基于高速缓存区 的边信道和恶意网站通信,这样就不用通过网络传输用户的隐私数据,进而不会触发操作系统的信息流跟踪功能。</p> <p>这个研究案例受到了来自某个安全部门的 “RF retro-reflector” 设计的启发,在这个设计里一台诸如麦克风的收集器,并不会把接收到的信号直接发送出去, 而是把接受的信号调制到由一个外部 ”收集设备“ 发送给它的 “照射信号” 上去。</p> <h3>3.1.1 设计</h3> <p>隐藏信道的设计有两个需求:第一,保持发送端的简单,我们尤其不想让它执行 章节2.1 中的移除集算法。第二,因为接收端的移除集是非典型的, 它应该足够简单,这样接收端就可以搜索到发送端的信号调制到了哪一个组。</p> <p>为了满足这些需求,我们的发射器/ APT 在自己的内存中分配了 4K 大小的数组,并且不断地把收集到的数据转换成对与这个数组的内存访问的模式。这个 4K 大小 的数组覆盖了缓存的 64 个组,这样 APT 在每个时间周期里就能传送 64比特 的数据。为了能保证内存访问能够被接收端定位,相同的访问模式被重复 运用到数组的几个拷贝上。 因此,高速缓冲区的大部分都会被执行到,与之形成对比的是 文章[23] 中的方法使用了典型的移除集,因此只会激活两条缓存线。</p> <p>接收端的代码会对操作系统的内存做一个性能分析,然后搜索含有被 APT 调制后的数据所在的页框。真正的数据会被从内存访问的模式里解调出来然后传回服务器, 整个过程都不会违背操作系统对信息流跟踪的保护。</p> <h3>3.1.2 评估</h3> <p>我们的攻击模型假设发送端使用(相对较快的)机器语言编写,而接收端是用 JavaScript 编写。所以,我们假设整个系统性能的限制因素是恶意网站的 采样速度。</p> <p>为了评估隐藏信道的带宽,我们写了一个小程序用预先设定好的模式来遍历系统的内存(即含有单词&quot;Usenix&quot;的比特图)。 接下来,我们用 JavaScript 高速缓存攻击来尝试寻找这一访问模式,并测量 JavaScript 代码所能达到的最大的频率。</p> <p><center><img src="/images/cache_attack_in_javascript/figure6.png" alt="图6"></center></p> <p><center>图6 主机到主机的隐藏信道</center></p> <p>图6 显示的内存谱图捕捉到了这一隐藏信道的执行。隐藏信道的理论带宽通过测量大约是 320kbps,这和 文章[23] 中用机器语言实现的跨虚拟机的隐藏信道 1.2Mbps 的带宽比较吻合。</p> <p><center><img src="/images/cache_attack_in_javascript/figure7.png" alt="图7"></center></p> <p><center>图6 主机到虚拟机的隐藏信道</center></p> <p>图7 中的内存谱图比较相似,但并不是由运行在相同主机上的接收端代码得到的,而是在一台虚拟机上得到的(Firefox 34 浏览器,Ubuntu 14.01 系统, VMWare Fusion 7.1.0 )。尽管在这个场景下的高峰频率只能到大约 8kbps,但是一个虚拟机中的网页竟然能够探测到底层的硬件,这着实让人惊讶。</p> <h2>4 利用高速缓存区攻击跟踪用户行为</h2> <p>绝大多数关于高速缓存区攻击的研究都假设攻击者和受害者位于云计算供应商数据中心里的同一台机器上。这样的机器一般不会配置成接收交互式的输入, 所以该领域的大部分研究都聚焦于如何获得加密钥匙或其它保密的状态信息,譬如随机数生成器的状态[26]。 本文将研究怎么利用高速缓存区攻击来跟踪用户的行为 ,这和我们的攻击模型更相关。我们注意到 文章[20] 已经尝试了利用 CPU 的一级缓存对系统负载进行细粒度的度量,以此跟踪按键事件的方法。</p> <p>本案例将演示一个恶意网站怎么用高速缓存区攻击去跟踪用户的活动。在接下来展示的的攻击里,我们会假设用户在一个背景标签页或窗口里打开 了一个恶意网站的页面,并且在另一个标签页或是另外一个完全没有互联网连接的应用里执行了一些敏感的操作。</p> <p>我们选择了把焦点集中在鼠标操作和网络活动上,因为操作系统负责处理它们的代码没有办法被忽略不计。所以,我们期待这些操作会在高速缓存区留下比较大的脚印。 而且正如下文所述,它们也很容易被 JavaScript 处处受限的安全模型所触发。</p> <h3>4.1 设计</h3> <p>两种攻击的结构比较类似。首先,进行性能分析,攻击者用 JavaScript 探测每一个组。接着,在训练阶段,待检测的活动(网络活动或鼠标操作)被触发, 伴随着对高速缓存区的高精度的采样。在训练阶段一方面通过测量脚本直接触发网络活动(执行一个网络请求),另一方面是不停地在网页上摇晃鼠标。</p> <p>通过比较在训练阶段缓存区在闲时和忙时的活动,攻击者可以知道用户操作会对应激活哪部分的组,并且训练出一个关于组的分类器。最后, 在分类阶段,攻击者不停地监视这些有意思的组从而掌握用户的活动。</p> <p>我们用一个基本的非结构化的训练过程,即假设训练过程中系统进行的最集中的操作就是被测量的。为了利用这点,我们计算了随着时间的 每次测量的 Hamming 权重(等于在某个周期内活跃组的个数),之后应用 k-meas 算法对测量数据做聚类。 然后计算每个簇中每个组的平均访问延迟,从而算出每个簇的中心。遇到未知的测量矢量,我们会计算这个矢量和各个中心的欧几里得距离, 并把它归到最近的那一类。</p> <p>在分类阶段,我们用命令行工具 wget 生成网络流量,并且将鼠标移动到窗口以外。为了获得网络活动的真实数据,我们同时用 tcp-dump 来测量系统的流量,然后把 tcp-dump 记录的时间戳和分类器所检测到时间戳联系起来。为了获得鼠标操作的真实数据,我们写了一个页面记录所有 鼠标事件及其时间戳。需要强调的是,记录鼠标活动的页面并不在运行着测量代码的浏览器(Firefox),而是运行在另一个浏览器里(Chrome)。</p> <h3>4.2 验证</h3> <p><center><img src="/images/cache_attack_in_javascript/figure8.png" alt="图8"></center></p> <p><center>图8 检测到的网络活动 </center></p> <p><center><img src="/images/cache_attack_in_javascript/figure9.png" alt="图9"></center></p> <p><center>图9 检测到的鼠标活动 </center></p> <p>活动测量的结果见 图8 和 图9 。两个图片的顶端都显示了高速缓存区一个子集的实时活动。图片的底端是分类器的输出结果和外部收集的真实数据。 正如图片展示的那样,我们异常简单的分类器在识别鼠标操作和网络活动方面非常有效。毫无疑问,使用更加 高级的训练和分类技巧可以进一步提高攻击的效果。需要强调的是,鼠标操作的检测器并不会检测网络活动,反之亦然。</p> <p>分类器的测量频率只有 500Hz。结果就是,它没有办法统计单个的包,而只能说明在一个阶段里活跃还是不活跃。另一方面,检测鼠标活动的代码要比 记录真实数据的代码采集到的事件多。这是因为 Chrome 浏览器对鼠标事件的频率做了限制,大约是 60Hz。</p> <p>Chen 等人在一篇著名的文章[5]中证明了对网络活动的监测可以作为深度挖掘用户行为的奠基石 。虽然 Chen 等人假设攻击者可以在网络层 监控受害者所有流入和流出的数据,但是这里所展示的技术本质上可以让恶意网站监控用户同时进行的网络操作。攻击可以被更多的指标增强, 例如内存分配(见文[13]),DOM 布局事件,磁盘写操作等。</p> <h2>5 结论</h2> <p>本文显示了边信道攻击的范围要比预期的大很多。本文提出的攻击可针对互联网上的大部分机器而不局限于某些特定的攻击场景。如此众多的系统突然间易受 边信道攻击意味着防止边信道攻击的算法和系统应当被广泛使用,而不能只是在某些特定情况下。</p> <h3>5.1 易被攻击系统的普遍性</h3> <p>我们的攻击需要一台个人计算机,并配有 Intel CPU,使用了 Sandy Bridge, Ivy Bridge, Haswell 或者 Broadwell 的微架构。据 IDC 的数据显示,2011年 以后售出的个人计算机80%都满足这一条件。更进一步,假设用户使用的浏览器支持 HTML5 高精度计时器和类型数组的规范。表1 列举了各个浏览器厂商支持这些 API 的最早的版本和易被攻击的版本占全球互联网流量的比重,统计数据来自 StatCounter GlobalStatas 2015年一月份的报告。如表所示,目前市场上 80% 的浏览器 都无法抵御此类攻击。</p> <table><thead> <tr> <th>Browser brand</th> <th>High Resolution Time Support</th> <th>Typed Arrays Support</th> <th>Worldwide prevalence</th> </tr> </thead><tbody> <tr> <td>Internet Explorer</td> <td>10</td> <td>11</td> <td>11.77%</td> </tr> <tr> <td>Safari</td> <td>8</td> <td>6</td> <td>1.86%</td> </tr> <tr> <td>Chrome</td> <td>20</td> <td>7</td> <td>50%</td> </tr> <tr> <td>Firefox</td> <td>15</td> <td>4</td> <td>17.67%</td> </tr> <tr> <td>Opera</td> <td>15</td> <td>12.1</td> <td>1.2%</td> </tr> <tr> <td>Total</td> <td>-</td> <td>-</td> <td>83.03%</td> </tr> </tbody></table> <p>攻击能否取得效果取决于能不能用 JavaScript 高精度时间API 进行精准的测量。虽然 W3C 对这个API规范定义了高精度时间的单位是 “毫秒,并精确到千分之一”, 但是它并没有给出该值的最高分辨率,实际上浏览器不同、操作系统不同这个值也会有所区别。举个例子,我们在测试过程中发现 MacOS 上的 Safari 浏览器可以精确 到纳秒,而 Windows 上的 IE 浏览器只能精确到 0.8 微秒。 另一方面, Chrome 浏览器在所有我们测试的操作系统上给出的分辨率都是 1 微秒。</p> <p>所以 图3 中,单次缓存命中和缓存未命中的差别大概是 50 纳秒,性能分析和测量的脚本在时间分辨率力度更细的操作系统上要稍加改动。 在性能分析阶段,我们没有统计单次未命中的时间,而是重复读取内存来放大时间的差别。 在测量阶段,我们虽然没有办法放大每次缓存未命中的时间,但是我们可以利用来自相同页框的代码通常会让相邻的组失效这一点。只要同一个页框的64个组里有20个 产生了缓存未命中,我们的攻击就可以在哪怕是毫秒级的分辨率上进行。</p> <p>我们提出的这种攻击还可以很容易的运用到手机、平板等移动设备上。值得一提的是,安卓浏览器从 4.4 版本开始支持 高精度时间API 和 类型数组,但在 本文撰写的时候 iOS Safari (8.1) 还不支持 高精度时间API。</p> <h3>5.2 反制措施</h3> <p>本文描述的攻击之所以可行,是因为它聚集了从微架构这一层到最终的 JavaScript 运行时设计和实现的的一些决定: 怎么把物理内存的地址映射到组, 嵌套的高速缓存区架构,JavaScript 高速的内存访问和高精度计时器;最后是 JavaScript 的权限模型。这里的每一点上都可以采取一些缓解措施, 但是都会对系统的正常使用产生影响。</p> <p>在<strong>微架构</strong>这层,修改物理内存到缓存线的映射方式可以非常有效地阻止我们的攻击,即不再用地址底12比特中的6个直接选择一个组。类似的,换用非嵌套的 缓存微架构而不是用嵌套的,会让我们的代码几乎不肯能精确地一级缓存中淘汰某项,使得测量更加困难。然而,这两个设计决定当初被选择正是为了让 CPU 的设计 和高速缓存的使用更高效,改变它们会让其它很多的应用性能受到影响。再说了,修改 CPU 微架构可不是一件小事,因为升级已经部署的硬件是肯定不行的。</p> <p>在<strong>JavaScript</strong>这层,似乎降低高精度计时器的分辨率就可以让攻击更难发动。但是,高精度计时器的建立是为了解决 JavaScript 开发者的实际需要的, 这些应用范围可从音乐和游戏再到增强现实和远程医疗。</p> <p>一个可能的权宜之计是限制应用只有在获得了用户许可后才能访问计时器(例如,通过显示一个确认窗口)或者通过第三方的认可(例如下载自可信的 “app store”)。</p> <p>一种有意思的方式是使用启发式的性能分析来检测和阻止此类攻击。如 Wang 等人利用大量算法和按位指令的存在可以预示这密码学应用元素 [21] 的存在, 可以注意到我们的攻击里各种测量步骤访问内存也会有一定的模式。因为现代的 JavaScript 的运行时,作为性能分析引导优化的机制的一部分,已经能够 详细的检查代码的运行时性能。所以 JavaScript 运行时应该能够在执行时发现有性能分析行为的代码并且相应的修改返回结果 (例如,在高精度计时器里加上抖动,或者动态的调整数组在内存中的位置等)。</p> <h3>5.3 结论</h3> <p>在这篇论文里,我们展示了如何有效地通过可疑网页发起针对微架构的边信道攻击,这种方式已被认为是非常有效的。 与高速缓存区攻击一般被用于密码分析应用不同,本文介绍了它怎么被用来有效地跟踪用户行为。 边信道攻击的范围已被拓展,这意味着设计新的安全系统时一定要考虑到对边信道攻击的反制措施。</p> <h2>致谢</h2> <p>我们很感谢激 Henry Wong 对于 Ivy Bridge 缓存淘汰策略的研究和 Burton Rosenberg 对于页和页框的讲解。</p> <h2>参考文献</h2> <p>见原文</p> <hr> <h2>关于译文</h2> <blockquote> <p>偶然看到这篇文章 <a href="http://www.cnbeta.com/articles/387087.htm">美发现新的浏览器攻击模式:可监控全球八成PC</a>, 非常惊讶 便随手点开文章里的原文连接 <a href="http://arxiv.org/pdf/1502.07373v2.pdf">PDF</a>,读完有种脑洞大开的感觉。论文知识点涉及计算机组成原理、虚拟化、 性能分析、JavaScript、HTML5规范等,都是我所感兴趣的领域,再加上中招的是 <a href="https://cn.linkedin.com/in/longtian">老东家</a> 的产品, 因此虽然我是安全领域的小白,但还是尝试精读并且斗胆翻译一下,不足之处欢迎大家批评指正。任何意见和建议请猛戳 <a href="https://github.com/wyvernnot/cache_attack_in_javascript/issues">issues</a></p> </blockquote> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】使用Rails 4.2+ 测试异步邮件系统 /ruby/2015/05/26/test-rails-mailer/ Tue, 26 May 2015 00:00:00 +0000 /ruby/2015/05/26/test-rails-mailer <p>原文链接:<a href="https://blog.engineyard.com/2015/testing-async-emails-rails-42">Testing async emails, the Rails 4.2+ way</a></p> <p>假设想写一个需要发送邮件的应用,我们都知道在这种情况是绝对不能block控制器的,因此异步传送才是解决之道。为了达到这个目的,我们需要 将邮件发送代码从最初的request/response循环中移到后台的异步处理进程中去。</p> <p>然而,做出这样的改变之后,我们如何确保代码能够一如往常的运行呢?在这篇博文中,我们会探索一种新方法来进行测试,我们将要使用的<code>MiniTest</code>(Rails已经内置了这个框架), 这里的概念同样使用<code>Rspec</code>。</p> <p>现在有一个好消息,那就是从Rails 4.2开始,异步传送邮件已经比之前简单多了。我们在例子中使用<code>Sidekiq</code>作为队列系统。由于<code>ActionMailer#deliver_later</code>建立在<code>ActiveJob</code>之上, 接口非常的简洁明了。这表示,要不是我刚才提了一下,身为开发者或用户的你也不会知情。建立队列系统是另外一个话题, 你可以在<a href="https://blog.engineyard.com/2014/getting-started-with-active-job">getting started with Active Job here</a>中了解更多相关的信息。</p> <h2>别太依赖小组件</h2> <p>在例子中,我们假定<code>Sidekiq</code>及其依赖组件配置正确,因此本场景特有的一段代码是声明<code>Active Job</code>该使用哪一个队列调节器。</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># config/application.rb</span> <span class="k">module</span> <span class="nn">OurApp</span> <span class="k">class</span> <span class="nc">Application</span> <span class="o">&lt;</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Application</span> <span class="c1"># ...</span> <span class="n">config</span><span class="o">.</span><span class="n">active_job</span><span class="o">.</span><span class="n">queue_adapter</span> <span class="o">=</span> <span class="ss">:sidekiq</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>Active Job在隐藏实质性的队列配置细节方面功能非常强大,以至于若是使用<code>Resque</code>,<code>Delayed Job</code>或其他组件,代码也不需要太大的改动。 因此,如果我们转而使用<code>Sucker Punch</code>,唯一的改变就是在引用相应的依赖包后,将<code>queue_adapter</code>从<code>:sidekiq</code>改为<code>:sucker_punch</code>就可以了。</p> <h2>站在Active Job的肩膀上</h2> <p>如果你是Rails 4.2或者<code>Active Job</code>不太了解,<a href="Ben%20Lewis&#x27;s%20intro%20to%20Active%20Job">https://blog.engineyard.com/2014/getting-started-with-active-job</a> 是就是很好的入门读物。然而,这篇文章留给我的一个小期许是,找到一种简洁、地道的测试方法,从而让所有组件都能正常的运行。</p> <p>根据本文的目标,我们假定你已经部署了:</p> <ul> <li>Rails 4.2或者一个更高的版本</li> <li>已经设置好<code>queue_adapter</code>的Active Job (Sidekiq, Resque, 等)</li> <li>一封邮件</li> </ul> <p>任何邮件都应该能够按照这里描述的方式正常工作,这里我们就用一封欢迎邮件来使这个例子更实用:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#app/mailers/user_mailer.rb</span> <span class="k">class</span> <span class="nc">UserMailer</span> <span class="o">&lt;</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span> <span class="n">default</span> <span class="ss">from</span><span class="p">:</span> <span class="s1">&#39;email@example.com&#39;</span> <span class="k">def</span> <span class="nf">welcome_email</span><span class="p">(</span><span class="ss">user</span><span class="p">:)</span> <span class="n">mail</span><span class="p">(</span> <span class="ss">to</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">email</span><span class="p">,</span> <span class="ss">subject</span><span class="p">:</span> <span class="s2">&quot;Hi </span><span class="si">#{</span><span class="n">user</span><span class="o">.</span><span class="n">first_name</span><span class="si">}</span><span class="s2">, and welcome!&quot;</span> <span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>为了保持程序简单并有针对性,我们会在每个用户注册后发送给他们一封欢迎邮件。</p> <p>这和<a href="http://guides.rubyonrails.org/action_mailer_basics.html#calling-the-mailer">the Rails guides mailer example</a>是一样的:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app/controllers/users_controller.rb</span> <span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span> <span class="k">def</span> <span class="nf">create</span> <span class="c1"># Yes, Ruby 2.0+ keyword arguments are preferred</span> <span class="no">UserMailer</span><span class="o">.</span><span class="n">welcome_email</span><span class="p">(</span><span class="ss">user</span><span class="p">:</span> <span class="vi">@user</span><span class="p">)</span><span class="o">.</span><span class="n">deliver_later</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <h2>The Mailer Should Do Its Job, Eventually</h2> <p>接下来,我们想确保控制器内的任务能如所期待的那样执行。</p> <p>在测试指南中,<a href="http://guides.rubyonrails.org/testing.html#custom-assertions-and-testing-jobs-inside-other-components">custom assertions for testing jobs inside other components</a> 的章节介绍了大约六种这样的自定义断言方法。</p> <p>或许直觉告诉你应该单刀直入,然后使用<code>assert_enqueued_jobs</code> <code>assert-enqueued-jobs</code> 来测试每次添加新用户时,我们有否将邮件传送任务放入队列。</p> <p>你可能会这么做:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># test/controllers/users_controller_test.rb</span> <span class="nb">require</span> <span class="s1">&#39;test_helper&#39;</span> <span class="k">class</span> <span class="nc">UsersControllerTest</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span> <span class="nb">test</span> <span class="s1">&#39;email is enqueued to be delivered later&#39;</span> <span class="k">do</span> <span class="n">assert_enqueued_jobs</span> <span class="mi">1</span> <span class="k">do</span> <span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="p">{}</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>然而如果这么做,你会惊奇地发现测试失败了,系统会告诉你<code>assert_enqueued_jobs</code>未经定义,且无法使用。</p> <p>这是因为,我们的测试类继承自<code>ActionController::TestCase</code>,而后者在编写时没有包含<code>ActiveJob::TestHelper</code>。</p> <p>不过我们很快就可以修正这一点:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># test/test_helper.rb</span> <span class="k">class</span> <span class="nc">ActionController</span><span class="o">::</span><span class="no">TestCase</span> <span class="kp">include</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">TestHelper</span> <span class="k">end</span> </code></pre></div> <p>假定我们的代码如期执行,那么测试应该就能顺利通过了。</p> <p>这是好消息。现在,我们可以重构我们的代码,增加新的功能,也可以增加新的测试。我们可以选择后者,看看我们的邮件有否投递成功,如果是的话,那就检查投递的内容是否正确。</p> <p><code>ActionMailer</code>能为我们提供一个包含所有发出邮件的队列,前提是将<code>delivery_method</code>选项设置为:<code>test</code>,我们能通过<code>ActionMailer::Base.deliveries</code>读取这个队列。</p> <p>当同步的地投递邮件时,检测邮件是否发送成功是很容易的。我们只需检查在动作完成后,投递计数器加1。用<code>MiniTest</code>来写的话,就像下面这样:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">assert_difference</span> <span class="s1">&#39;ActionMailer::Base.deliveries.size&#39;</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span> <span class="k">do</span> <span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="p">{}</span> <span class="k">end</span> </code></pre></div> <p>我们的测试是实时发生的,但在开篇就已经知道不能阻拦控制器,需要在后台进程中发送邮件,现在我们把所有的组件都组装起来,确定系统是没有问题的。 因此,在异步的世界里,我们必须先执行所有队列中的任务再去判定执行结果。为了执行pending中的<code>Active Job</code>任务,我们使用<code>perform_enqueued_jobs</code>:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">test</span> <span class="s1">&#39;email is delivered with expected content&#39;</span> <span class="k">do</span> <span class="n">perform_enqueued_jobs</span> <span class="k">do</span> <span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="p">{}</span> <span class="n">delivered_email</span> <span class="o">=</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">deliveries</span><span class="o">.</span><span class="n">last</span> <span class="c1"># assert our email has the expected content, e.g.</span> <span class="n">assert_includes</span> <span class="n">delivered_email</span><span class="o">.</span><span class="n">to</span><span class="p">,</span> <span class="vi">@user</span><span class="o">.</span><span class="n">email</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <h2>缩短反馈流程</h2> <p>目前为止,我们都在进行功能性测试以确保我们的控制器如期执行。但是,代码的变化足以破坏我们发送的邮件,为什么不对我们的邮件程序进行单元测试,从而缩短反馈流程,然后更快地洞察变化呢?</p> <p>Rails测试指南建议在这里使用fixtures,但是我觉得他们太生硬了。尤其是一开始,当我们还在尝试设计邮件时,一个变化很快就会让他们变得不可用,让我们的测试无法通过。 我偏向使用<code>assert_match</code>以关注那些构成邮件主体的关键元素。</p> <p>为此,也因为其他原因(比如抽离处理多部分邮件的逻辑结构),我们可以建立自定义断言。这可以扩展<code>MiniTest</code>标准断言或Rails专属断言。 这也是创建自己的领域专属语言(Domain Specific Language)并用于测试的好例子。</p> <p>让我们在测试一文件夹内创建一个共享文件夹,用以存放<code>SharedMailerTests</code>模块。我们自定义的断言可以这么来写:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># /test/shared/shared_mailer_tests.rb</span> <span class="k">module</span> <span class="nn">SharedMailerTests</span> <span class="k">def</span> <span class="nf">assert_email_body_matches</span><span class="p">(</span><span class="ss">matcher</span><span class="p">:,</span> <span class="ss">email</span><span class="p">:)</span> <span class="k">if</span> <span class="n">email</span><span class="o">.</span><span class="n">multipart?</span> <span class="sx">%w(text html)</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">part</span><span class="o">|</span> <span class="n">assert_match</span> <span class="n">matcher</span><span class="p">,</span> <span class="n">email</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="s2">&quot;</span><span class="si">#{</span><span class="n">part</span><span class="si">}</span><span class="s2">_part&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">body</span><span class="o">.</span><span class="n">to_s</span> <span class="k">end</span> <span class="k">else</span> <span class="n">assert_match</span> <span class="n">matcher</span><span class="p">,</span> <span class="n">email</span><span class="o">.</span><span class="n">body</span><span class="o">.</span><span class="n">to_s</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>接下来,我们需要让邮件测试系统注意到这个自定义断言,为此,我们可以将其放入<code>ActionMailer::TestCase</code>类中。 然后可以借鉴之前把<code>ActiveJob::TestHelper</code>类包含于<code>ActionController::TestCase</code>类的方法:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># test/test_helper.rb</span> <span class="nb">require</span> <span class="s1">&#39;shared/shared_mailer_tests&#39;</span> <span class="k">class</span> <span class="nc">ActionMailer</span><span class="o">::</span><span class="no">TestCase</span> <span class="kp">include</span> <span class="no">SharedMailerTests</span> <span class="k">end</span> </code></pre></div> <p>注意,我们首先需要在<code>test_helper</code>中<code>require shared_mailer_tests</code>。</p> <p>这些办好之后,我们现在可以确信我们的邮件中包含我们期望的关键元素。假设我们想确保发送给用户的URL包含一些用于追踪的特定UTM参数。 我们现在可以将自定义断言与老朋友<code>perform_enqueued_jobs</code>联合起来使用,就像这样:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># test/mailers/user_mailer_test.rb</span> <span class="k">class</span> <span class="nc">ToolMailerTest</span> <span class="o">&lt;</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">TestCase</span> <span class="nb">test</span> <span class="s1">&#39;emailed URL contains expected UTM params&#39;</span> <span class="k">do</span> <span class="no">UserMailer</span><span class="o">.</span><span class="n">welcome_email</span><span class="p">(</span><span class="ss">user</span><span class="p">:</span> <span class="vi">@user</span><span class="p">)</span><span class="o">.</span><span class="n">deliver_later</span> <span class="n">perform_enqueued_jobs</span> <span class="k">do</span> <span class="n">refute</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">deliveries</span><span class="o">.</span><span class="n">empty?</span> <span class="n">delivered_email</span> <span class="o">=</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">deliveries</span><span class="o">.</span><span class="n">last</span> <span class="sx">%W(</span> <span class="sx"> utm_campaign=</span><span class="si">#{</span><span class="vi">@campaign</span><span class="si">}</span><span class="sx"></span> <span class="sx"> utm_content=</span><span class="si">#{</span><span class="vi">@content</span><span class="si">}</span><span class="sx"></span> <span class="sx"> utm_medium=email</span> <span class="sx"> utm_source=mandrill</span> <span class="sx"> )</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">utm_param</span><span class="o">|</span> <span class="n">assert_email_body_matches</span> <span class="n">utm_param</span><span class="p">,</span> <span class="n">delivered_email</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <h2>结论</h2> <p>在<code>Active Job</code>的基础上,使用<code>ActionMailer</code>让从同步发送邮件到通过队列发送邮件的转化变得如此简单,就如同从<code>deliver_now</code>转化到<code>deliver_later</code>。</p> <p>同时,由于使用<code>Active Job</code>大大简化了设定工作基础环境的流程,你可以对自己所用的队列系统知之甚少。希望这篇教程能让你对此过程有更多了解。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】影响网页渲染的关键! /javascript/2015/05/21/render/ Thu, 21 May 2015 00:00:00 +0000 /javascript/2015/05/21/render <p>原文地址:<a href="http://www.feedthebot.com/pagespeed/critical-render-path.html">Critical rendering path</a></p> <p>经常有站长、开发者、运维疑惑:为什么我们的后台服务器很快,但是用户要看网页里面的内容却需要很长时间?我们在上一篇文章《怪兽大作战: 解析网站打开慢的原因》【http://news.yesky.com/prnews/420/58034920.shtml】中简单介绍了影响网站打开速度的几个指标,感兴趣的同学可以再读一下。今天我们主要讲一下,是哪些因素拖慢了我们的首屏加载时间,也就是用户看到网页中内容时所等待的时间。</p> <p><img src="/images/render/1.png" alt="ruby-prof"> 用过OneAPM的读者对这幅图肯定不陌生,一般来讲,如果服务器很快,机房所在线路很快,那么影响用户看到网页内容的主要时间,就是最后两个时间阶段:DOM处理以及网页渲染,在这两个阶段中,浏览器需要解析网页中的各种资源并进行渲染,最终形成用户页面。这个过程是否流畅,直接影响到用户需要等待的时间,从更深层次而言,直接会影响最终的用户体验,现在大家也普遍接受一个观点“延迟就是故障”,所以你需要重视网站的加载速度。</p> <h2>打造轻量级的资源路径--关键渲染路径</h2> <p>网页加载速度中最重要的概念是关键渲染路径。如果能理解好这个概念,的确可以让用户更快看到网页中的内容。</p> <p>轻量级资源和路径,可以缩短复杂网页的构建和渲染时间,甚至比简单网页还要快! 由于大多数网页都包含许多不同的组成部分,仅仅移除部分资源并不能保证更快的加载速度。 如果你曾经想过:“为了提高网页的加载速度,我还能做什么?”或者“新浪、QQ、网易是如何做到在一秒钟内加载那么多网页内容的?”那么关键渲染路径这个概念正是你需要了解的。</p> <h2>什么是关键渲染路径?</h2> <p>清楚起见,让我们先定义一些概念:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">关键:绝对需要 渲染:显示或者展示(在我们的情境中,网页经过渲染才能呈现给用户) 路径:使我们的网页展示在浏览器中的一系列事件 首屏:是用户滚动页面之前就能看见的部分。 </code></pre></div> <p>因此,换言之,渲染路径就是一系列使你的网页呈现在浏览器中的事件。而关键渲染路径是呈现网页首屏所需的那些事件。因为几乎所有网站在渲染网页时都包含了不必要的步骤,而减少这些不必要的路径,能使你的网页加载速度提高几秒钟,这也是提高网页速度的最快方法。</p> <h2>路径</h2> <p>为了显示一张网页,浏览器必须获取网页所需的所有资源。一个简单的例子:一个网页需要一张图片,一个CSS文件,一个JavaScript文件。</p> <p>我们来看看这张网页在展示之前经历的路径:</p> <ol> <li> 浏览器下载html文件</li> <li> 浏览器读取html文件,发现里面涉及一个CSS文件,一个JavaScript文件和一张图片</li> <li> 浏览器开始下载这张图片</li> <li> 浏览器发现不获取CSS和JavaScript文件就无法显示网页</li> <li> 浏览器下载CSS文件并读取之,确保除此之外没有别的文件需要被访问</li> <li> 浏览器发现不获取JavaScript文件还是无法显示网页</li> <li> 浏览器下载JavaScript文件并读取之,确保除此之外没有别的文件需要被访问</li> <li> 浏览器发现现在可以显示网页了</li> </ol> <p>上面的路径是简单网页的加载过程。现在,试想一下你的网页加载路径会怎么样?你很可能会有几个交互按钮,数个CSS和JavaScript文件,很多图片和小插件,甚至可能还有音频或者视频文件。这意味着,你的渲染路径很可能会像一个大迷宫。大多数网站的渲染路径都极其复杂,因为浏览器在显示网页之前需要加载的文件太多。这就是你可以超过他人的地方。如果你让自己的网页加载得比竞争者的快,你就能获得访问者的青睐(百度就喜欢这样的开发者),例如新浪微博的路径就是这样的: <img src="/images/render/2.png" alt="ruby-prof"></p> <h2>渲染过程</h2> <p>在展示网页所需的众多资源中,存在一些资源会阻塞网页的渲染过程。最常见的两种资源就是CSS文件和JavaScript文件。不管你需要多少个这样的文件,浏览器必须逐一下载并分析这些文件,然后才能给用户展示内容。让我们来看一个最常见不过的场景:</p> <p>WordPress博客使用主题。几乎每一个WordPress主题都包含多个CSS文件。</p> <p>许多主题包含六七个CSS文件。所有的CSS文件都可以合并到一个文件中,但是当你添加主题时,会包含多个CSS文件。因此,在你的博客显示哪怕一个字之前,浏览器都不得不经过六七次的与服务器交互,把这些文件一个个地下载下来,并分析读取,之后才能开始显示。</p> <p>在加载的过程中,访问者都只能看到一篇空白的屏幕。因为只有当关键步骤完成以后,才会有东西显示。</p> <p>但是,即便下载完这些CSS文件,你的博客还是不能完成渲染。因为WordPress主题还需要几个JavaScript文件。</p> <p>因此,渲染一页典型的WordPress博客网页,需要浏览器与服务器交互大约20次,才能将主要的CSS和JavaScript文件下载完毕。但是,等等,现在你还需要交互按钮,小插件……噢,不,针对每一个这样的部件,你还要下载几个CSS,JavaScript文件。</p> <p>你可能要下载几十个文件,才能让自己的博客展示在用户面前。真是麻烦!(去查查你的网页都要加载什么文件,可以使用OneAPM 的SessionTrace 功能看看网页加载资源在用户那里的速度)</p> <p>但是事情不仅限于WordPress,本文只是拿它举个例子而已。通常创建网页的初始视图都很多资源,因此会产生多个请求。 <img src="/images/render/3.png" alt="ruby-prof"></p> <h2>关键</h2> <p>目前我只是描绘了一张非常朦胧的蓝图。好消息是:你可以为你的网页请求一百万个资源,其中包括12000张图像,200个JavaScript文件,而这些都可能在一秒钟内加载完成。</p> <p>这是如何实现的呢?</p> <p>只要理解对你的网站而言,显示首屏的内容所需的关键步骤,就能实现。</p> <p>最优化渲染路径,实际上只要聚焦三件事情:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">最小化关键资源的数量 最小化关键字节数 最小化关键路径的长度 </code></pre></div> <h2>理解页面加载速度的测量办法</h2> <p>当百度谈论页面加载速度时,他们并不是指加载一个网页的总时间。他们说的是用户看到首屏所需的时间,以及用户可以开始与页面内容进行交互所需的时间。</p> <p>百度之所以开始采用页面加载速度作为影响要素,是基于他们用户的满意度。当用户使用百度搜索时,他们要是被带到加载时间很长的页面,无疑是很糟糕的经历。</p> <p>人们向百度抱怨,他们说:“为什么将我带到一个加载如此缓慢的页面?”显然,人们感知到了速度的差别。</p> <p>如果一个用户要盯着一个空白的网页10秒之久等待它加载内容,这无疑是很差的体验。百度不想在他们的结果中出现这样的页面。如果那个页面能在1秒内显示内容,这就是极好的用户体验,这才是百度想要的结果。</p> <p>我们讨论网页速度时最关注的就是将初首屏的内容尽早地显示给用户。 通过OneAPM SessionTrace 功能可以查看各个资源的加载速度,方便调整加载资源的策略,例如 <img src="/images/render/4.png" alt="ruby-prof"></p> <h2>后续:</h2> <p>其实,优化网页渲染路还有很多小技巧、插件、方法等待,未来我们将在后续的文章中一一和大家分享。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】元编程动态方法之public_send /ruby/2015/05/20/meta-programming-public-send/ Wed, 20 May 2015 00:00:00 +0000 /ruby/2015/05/20/meta-programming-public-send <p>原文地址:<a href="http://vaidehijoshi.github.io/blog/2015/05/05/metaprogramming-dynamic-methods-using-public-send">Metaprogramming Dynamic Methods: Using Public_send</a></p> <p>作者:Friends of The Web的开发者Vaidehi,OneAPM官方技术博客编译整理。</p> <p><img src="http://imgs.xkcd.com/comics/hofstadter.png" alt="pic1 "></p> <p>在上周,我写了一些让我感到非常骄傲的代码!当时,我正努力解决一个有趣的问题,这个问题也是我最近开发的一款应用中所遇到的。于是我把脑海中想到的第一种解决办法很快付诸了实践。然后,当我回过头来查看文本编辑器,并认真审阅完自己所写的代码时,终于意识到:这些代码真的很赞!</p> <p>一周以来,我不断回顾那些代码段,沉思到底是什么原因令她如此美丽。而我又做了什么不一样的事情,竟让自己的内心充满了骄傲。我认为是元编程,这是我能够第一时间想到的答案。元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在编译时完成部分本应在运行时完成的工作。当然,其中涉及到很多不同的技巧和方法,我也不是专家。但是我的确学到了一个元方法 public_send ,接下来我分享一下自己的使用心得。</p> <h2>GOTTA DISPATCH? DO IT DYNAMICALLY.</h2> <p>其实,不仅仅只是Ruby,编程世界中的一切都只是一种抽象。我们只不过对那些晦涩难懂的机器语言包裹了一层“糖衣”,让程序员的生活变得更为简便,让代码更具美感罢了。但归根结底,这只是对其他事物的一种抽象而已。当我们通过元编程进行重构时,理解的是同样的抽象概念,而且我们必须牢记这一点。同时,当我们试图发现代码中的共性问题时,这也意味着,我们可以封装并抽离那段代码的部分功能。</p> <p>我最喜欢的抽象实例是<code>method dispatching</code>,通过这种方法我们将信息传递给一个对象,当然,这也是我们经常做的事情。因为Ruby中的所有事物都是一个对象,只要你想调用某个对象完成某件任务,你就不得不向它发送信息。我们应该庆幸,因为Ruby是如此强大,我们用来发送信息的方法就刚好就叫做:send。</p> <p>其实,send方法在程序中被调用的次数远超过我们的想象。比如,当我们打开操控台,做一些简单的数学运算:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">2.2.0 &gt; 3 + 4 =&gt; 7 </code></pre></div> <p>而我们真正做事情的是:给整数3这个对象发送一个信息,告诉它与另一个对象(整数4)进行加法动作。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">2.2.0 &gt; 3.send(:+, 4) =&gt; 7 </code></pre></div> <p><code>send</code> 方法用字符串或者一个符号为参数,并以此做为方法名。换言之,方法名总是第一个参数,而第二个参数将以自变量的形式传给该方法。</p> <p>此时,如果你想执行3加4的加法操作,就会变得很容易。但是,谁会一直做如此简单的加法计算呢?显然没有。你很可能还会执行3加5,加6,一直加到无穷等等。</p> <p>而使用<code>dynamic dispatching</code> 就可以拯救你!</p> <p><img src="https://media.giphy.com/media/144RafbwKkBDva/giphy.gif" alt="pic 2"></p> <p>Dynamic dispatching,就如同上面这幅略显奇怪但是相当可爱的动态图,因为涉及向对象发送不同的信息<code>(read: methods)</code> ,同时因情况的差异,还会产生方法不断改变的警告。<code>Dynamic dispatching</code> 允许我们在程序中面向对象发送不同的方法,而且无需告知其他对象所发送信息的内容。如果你需要再一个特定的环境下调用一个方法,但是不清楚该方法是如何执行的,那么此时就是<code>Dynamic dispatching</code> 出场的最佳时机。</p> <p>我们可以看一下这个例子。</p> <h2>YOU CAN SEND WHUTEVA YOU LIKE</h2> <p>你可以使用 <code>send</code> 方法,来向一个对象“send”不同的方法,但这只占据了一半的乐趣,另一半在于搞清楚什么时候去实际的应用它。</p> <p>此刻,让我来给大家展示一下,最近如何在应用中使用 <code>Dynamic dispatching</code> 来触发特定的方法。在本文中,我使用了一个电子书店的例子,希望大家能够接受。</p> <p>在我的书店中,拥有一个开放购买的图书列表,列表以页码为单位来进行展示,且每本书在界面中只有有限的展示空间。作为网站的管理员,我必须决定针对不同的书应该如何进行更好的展示。一些书有非常漂亮的封面,我会想用封面的缩略图作为他们主要的“viewable attribute”,因为我使用了 <code>paperclip</code> 插件,这点很容易实现。</p> <p>然而,有些书压根就没有封面。比如,浩如烟海的莎士比亚戏剧集,如果以“作者”做为“viewable attribute”,能更好地能吸引读者。而《权力的游戏》系列图书若以书名作为“可见属性”,显然更具吸引力。</p> <p>所以,我该如何处理这个问题呢?首先,让我们看一下有没有任何共性的存在。</p> <h3>1. Look For Patterns</h3> <p>诚然,我们都希望界面中的每个 <code>Book</code> 对象都能用它最主要的可见属性来展示。我们遇到的问题是管理员会为每个 <code>Book</code> 对象选择不同的属性,再设置其为“viewable”,因此我们无法预测这个属性是<code>title</code> ,<code>author</code> ,还是是一张图像。但是,我们的确知道每个 <code>book</code> 对象都需要某个“viewable attribute”。</p> <p>这点很酷!所以这里就发现了一个共性:我们需要展示一个属性,但是我们并不知道它会是什么。或者说,其实我们知道?</p> <h3>2. Consider The Data</h3> <p>当下我们为该应用建立一个管理员界面时,我们清楚的知道每本书都有一个<code>title</code>,一个<code>author</code>。书的封面可以是有选择性的(此处我们称其为<code>media</code> ),但另外两个属性则不一定。这意味着我们要对 <code>Book</code> 对象进行一次验证:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Book</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span> <span class="n">validates_presence_of</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">:author</span> <span class="k">end</span> </code></pre></div> <p>经过验证值之后,我进一步思考<code>Book</code> 对象肯定也会有的其他属性,首先进入我脑海的就是<code>viewable_by</code>属性。我们可以设想一下,管理员必须将某个属性设为“viewable”,当他更新此对象时,“viewable”就可能改变。因此,对每个 <code>Book</code> 对象来说都是独一无二的,这也意味着我们可以放心地将其保存在数据库中。</p> <p>所以,我们可以通过代码迁移在数据库中增加<code>viewable_by</code>属性,并且设置成不可为<code>null</code>,同时将默认值设为<code>Book’s title</code>:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AddViewableByToBooks</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span> <span class="k">def</span> <span class="nf">change</span> <span class="n">add_column</span> <span class="ss">:books</span><span class="p">,</span> <span class="ss">:viewable_by</span><span class="p">,</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span><span class="p">,</span> <span class="ss">default</span><span class="p">:</span> <span class="s2">&quot;title&quot;</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>这个迁移看起来非常简单,但是,but it is its very simplicity that lends itself so elegantly to some serious metaprogramming that we’ll do next.</p> <h3>3. Encapsulate And Abstract</h3> <p>最后这部分的确最难理解,但也最炫酷。现在,我们的数据库里多了一列,且字符串类型的值是<code>title</code> , <code>author</code>,<code>media</code>中的一个,管理员可以随意改变或更新这些值。显而易见,这些值的更改不可避免,但是有一点不变:we’re still going to want to render the value of whatever attribute is marked as “visible” – that is to say, whatever string value is saved as <code>viewable_by</code> .</p> <p>此时,我们可以回头想想之前定义的那个共性。我们知道,属性会不断改变,但是我们对它的操作仍然保持了一致性。不论 <code>Book</code> 对象的<code>viewable_by</code>属性是什么,我们都会展示这个属性。而且我们会跟该对象发送信息:“嘿,书先生,你可见的那个属性,不论它是什么,都是你用来展示自己的值!”</p> <p>And this is where we can use <code>send</code> to encapsulate and abstract this away into a single method call。首先,我们要增加一个方法来检查可见属性是不是一个图象,如果是的话,我们会将其交给<code>paperclip</code>插件来进行展示:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">show_cover?</span> <span class="nb">self</span><span class="o">.</span><span class="n">viewable_by</span> <span class="o">==</span> <span class="s1">&#39;media&#39;</span> <span class="k">end</span> </code></pre></div> <p>如果<code>viewable_by</code>属性的值为<code>media</code>,该方法会返回true,否则返回false。我们可以将这个布尔返回值用在一个条件语句中:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">book_html</span> <span class="k">if</span> <span class="n">show_cover?</span> <span class="c1"># Code here will generate and return</span> <span class="c1"># an html image tag to render in view.</span> <span class="k">else</span> <span class="nb">send</span><span class="p">(</span><span class="nb">self</span><span class="o">.</span><span class="n">viewable_by</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>上面的代码怎么了?如此炫酷!<code>book_html</code>方法要么展示一张缩略图(我们会写别的代码来实现),要么返回一个<code>title</code> 对象或者<code>author</code>对象。当然,真正炫酷的地方在于,我们可以在表中添加其他属性,像<code>year</code>或者<code>genre</code>,并在此基础上显示html网页,只需要这些属性保存在<code>viewable</code>列中。</p> <p>这到底是怎么运行的?其实,每次我们在数据库里创建新列时,我们都获得了两个重要的方法:读和写。这意味着我们拥有了<code>title=</code>和 <code>title</code>两个方法。</p> <p>如果我们回顾一下 <code>send</code> 方法的工作原理,就会知道<code>send</code> 会将一个字符串或一个符号作为参数,该参数也是被调用的方法名。当我们调用 <code>send</code> 方法并将<code>self.viewable_by</code> 的值传给它时,我们实际上是在调用一个<code>Book</code>实例的<code>send(“title”)</code> 方法。这会调用该<code>Book</code>实例的<code>title</code> 属性,并将那本书的书名以字符串的形式返回。</p> <p>这段代码炫酷的地方在于它非常灵活,同时还能将一种共性抽象为动态的方法调用,只在合适的时间对恰当的对象发起请求。但是,这段代码中还存在一个大问题,接下来让我们把它解决掉。</p> <h2>TO SEND OR TO PUBLIC SEND? THAT IS THE QUESTION</h2> <p>很多控诉 <code>send</code> 方法的证据都源于此: <code>send</code> 会将<code>private methods</code>发送给对象。这在应用内部是相当危险的,而且也让应用在应对外部恶意攻击���显得相当脆弱。</p> <p>一种快速的修正方法就是使用 <code>pubic_send</code> ,它的功能与你所想的完全一致:只将公共可读取的方法发送给它的接收对象,所以我们最终的代码如下:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Book</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span> <span class="n">validates_presence_of</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">:author</span> <span class="k">def</span> <span class="nf">show_cover?</span> <span class="nb">self</span><span class="o">.</span><span class="n">viewable_by</span> <span class="o">==</span> <span class="s1">&#39;media&#39;</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">book_html</span> <span class="k">if</span> <span class="n">show_cover?</span> <span class="c1"># Code here will generate and return</span> <span class="c1"># an html image tag to render in view.</span> <span class="k">else</span> <span class="n">public_send</span><span class="p">(</span><span class="nb">self</span><span class="o">.</span><span class="n">viewable_by</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>赞!作为我们元编程的首次尝试,这段代码并没有显得很寒碜。</p> <p>虽然做起来很难,但在重构和元编程方面,你也不必太苛求自己。老实说,随着时间的累积,你可以多加练习,逐渐的增长见识,那么编程能力自然也能够得到提高。最终,你会开始察觉那些出现了一次又一次的共性,并开始学会选择正确的工具来解决这些问题。</p> <p>尽管需要额外的努力,我认为尝试不同的元编程技巧对未来大有裨益,多读多写代码亦是如此。重写之前的代码来实现一些元编程技巧,你可以修改应用中过于死板的代码,将之变得更为灵活、动态。</p> <p>如果这些话听起来有些吓人,那是因为事实本就如此!但是这并不是不可能的,正如我最近取得的进展,我也希望正在阅读此文的你也能有所斩获。幸运的是,Ruby提供了很多工具,帮我们将死板的代码实现了元程序化。而问题就在于了解那些工具,并在合适的时机能够使用它。我相信,如果你体验了一次元编程,你肯定会欣喜万分,也许还会像小猫一样发出欢乐的尖叫,我想那将是世上最可爱的事情了。</p> <p><img src="https://media.giphy.com/media/117j1ldyb838Zi/giphy.gif" alt="pic 3"></p> <h2>总结一下</h2> <ul> <li>在编写程序时,我们可以使用dynamic dispatching将一个方法传递给对象,而不必指明方法的内容。方法<code>send</code>和 <code>pubic_send</code>都能实现这一点,它们以字符串或符号为参数,并以此参数作为接受对象调用的方法名。</li> <li><a href="http://ruby-metaprogramming.rubylearning.com/html/ruby_metaprogramming_2.html">点击这里</a>学习元编程的基本要素,并查看<code>send</code>和<code>pubic_send</code>方法的相关文档。</li> <li>如果你还好奇其他的动态方法?可以阅读一下<a href="http://ruby.about.com/od/oo/ss/Dynamic-Method-Calls.htm">这篇博文</a>,其中深度介绍了一部分方法。</li> </ul> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】提高Python运行效率的六个窍门 /python/2015/05/18/python-performance-tips/ Mon, 18 May 2015 00:00:00 +0000 /python/2015/05/18/python-performance-tips <p>原文地址:<a href="https://blog.newrelic.com/2015/01/21/python-performance-tips/">https://blog.newrelic.com/2015/01/21/python-performance-tips/</a></p> <p>Python是一门优秀的语言,它能让你在短时间内通过极少量代码就能完成许多操作。不仅如此,它还轻松支持多任务处理,比如多进程。</p> <p>不喜欢Python的人经常会吐嘈Python运行太慢。但是,事实并非如此。尝试以下六个窍门,来为你的Python应用提速。</p> <h2>窍门一:关键代码使用外部功能包</h2> <p>Python简化了许多编程任务,但是对于一些时间敏感的任务,它的表现经常不尽人意。使用C/C++或机器语言的外部功能包处理时间敏感任务,可以有效提高应用的运行效率。这些功能包往往依附于特定的平台,因此你要根据自己所用的平台选择合适的功能包。简而言之,这个窍门要你牺牲应用的可移植性以换取只有通过对底层主机的直接编程才能获得的运行效率。以下是一些你可以选择用来提升效率的功能包:</p> <ul> <li><a href="http://cython.org/">Cython</a></li> <li><a href="http://pyinline.sourceforge.net/">Pylnlne</a></li> <li><a href="http://pypy.org/">PyPy</a></li> <li><a href="http://www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/">Pyrex</a></li> </ul> <p>这些功能包的用处各有不同。比如说,使用C语言的数据类型,可以使涉及内存操作的任务更高效或者更直观。Pyrex就能帮助Python延展出这样的功能。Pylnline能使你在Python应用中直接使用C代码。内联代码是独立编译的,但是它把所有编译文件都保存在某处,并能充分利用C语言提供的高效率。</p> <h2>窍门二:在排序时使用键</h2> <p>Python含有许多古老的排序规则,这些规则在你创建定制的排序方法时会占用很多时间,而这些排序方法运行时也会拖延程序实际的运行速度。最佳的排序方法其实是尽可能多地使用键和内置的sort()方法。譬如,拿下面的代码来说:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="kn">import</span> <span class="nn">operator</span> <span class="n">somelist</span> <span class="o">=</span> <span class="p">[(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">5</span><span class="p">)]</span> <span class="n">somelist</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">operator</span><span class="o">.</span><span class="n">itemgetter</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span> <span class="n">somelist</span> <span class="c">#Output = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]</span> <span class="n">somelist</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">operator</span><span class="o">.</span><span class="n">itemgetter</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="n">somelist</span> <span class="c">#Output = [(6, 2, 4), (1, 5, 8), (9, 7, 5)]</span> <span class="n">somelist</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">operator</span><span class="o">.</span><span class="n">itemgetter</span><span class="p">(</span><span class="mi">2</span><span class="p">))</span> <span class="n">somelist</span> <span class="c">#Output = [(6, 2, 4), (9, 7, 5), (1, 5, 8)],</span> </code></pre></div> <p>在每段例子里,list都是根据你选择的用作关键参数的索引进行排序的。这个方法不仅对数值类型有效,还同样适用于字符串类型。</p> <h2>窍门三:针对循环的优化</h2> <p>每一种编程语言都强调最优化的循环方案。当使用Python时,你可以借助丰富的技巧让循环程序跑得更快。然而,开发者们经常遗忘的一个技巧是:尽量避免在循环中访问变量的属性。譬如,拿下面的代码来说:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="n">lowerlist</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;this&#39;</span><span class="p">,</span> <span class="s">&#39;is&#39;</span><span class="p">,</span> <span class="s">&#39;lowercase&#39;</span><span class="p">]</span> <span class="n">upper</span> <span class="o">=</span> <span class="nb">str</span><span class="o">.</span><span class="n">upper</span> <span class="n">upperlist</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">append</span> <span class="o">=</span> <span class="n">upperlist</span><span class="o">.</span><span class="n">append</span> <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">lowerlist</span><span class="p">:</span> <span class="n">append</span><span class="p">(</span><span class="n">upper</span><span class="p">(</span><span class="n">word</span><span class="p">))</span> <span class="k">print</span><span class="p">(</span><span class="n">upperlist</span><span class="p">)</span> <span class="c">#Output = [&#39;THIS&#39;, &#39;IS&#39;, &#39;LOWERCASE&#39;]</span> </code></pre></div> <p>每次你调用str.upper, Python都会计算这个式子的值。然而,如果你把这个求值赋值给一个变量,那么求值的结果就能提前知道,Python程序就能运行得更快。因此,关键就是尽可能减小Python在循环中的工作量。因为Python解释执行的特性,在上面的例子中会大大减慢它的速度。</p> <p>(注意:优化循环的方法还有很多,这只是其中之一。比如,很多程序员会认为,列表推导式是提高循环速度的最佳方法。关键在于,优化循环方案是提高应用程序运行速度的上佳选择。)</p> <h2>窍门四:使用较新的Python版本</h2> <p>如果你在网上搜索Python,你会发现数不尽的信息都是关于如何升级Python版本。通常,每个版本的Python都会包含优化内容,使其运行速度优于之前的版本。但是,限制因素在于,你最喜欢的函数库有没有同步更新支持新的Python版本。与其争论函数库是否应该更新,关键在于新的Python版本是否足够高效来支持这一更新。</p> <p>你要保证自己的代码在新版本里还能运行。你需要使用新的函数库才能体验新的Python版本,然后你需要在做出关键性的改动时检查自己的应用。只有当你完成必要的修正之后,你才能体会新版本的不同。</p> <p>然而,如果你只是确保自己的应用在新版本中可以运行,你很可能会错过新版本提供的新特性。一旦你决定更新,请分析你的应用在新版本下的表现,并检查可能出问题的部分,然后优先针对这些部分应用新版本的特性。只有这样,用户才能在更新之初就觉察到应用性能的改观。</p> <h2>窍门五:尝试多种编码方法</h2> <p>每次创建应用时都使用同一种编码方法几乎无一例外会导致应用的运行效率不尽人意。可以在程序分析时尝试一些试验性的办法。譬如说,在处理字典中的数据项时,你既可以使用安全的方法,先确保数据项已经存在再进行更新,也可以直接对数据项进行更新,把不存在的数据项作为特例分开处理。请看下面第一段代码:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="n">n</span> <span class="o">=</span> <span class="mi">16</span> <span class="n">myDict</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span> <span class="n">char</span> <span class="o">=</span> <span class="s">&#39;abcd&#39;</span><span class="p">[</span><span class="n">i</span><span class="o">%</span><span class="mi">4</span><span class="p">]</span> <span class="k">if</span> <span class="n">char</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">myDict</span><span class="p">:</span> <span class="n">myDict</span><span class="p">[</span><span class="n">char</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span> <span class="n">myDict</span><span class="p">[</span><span class="n">char</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span> <span class="k">print</span><span class="p">(</span><span class="n">myDict</span><span class="p">)</span> </code></pre></div> <p>当一开始myDict为空时,这段代码会跑得比较快。然而,通常情况下,myDict填满了数据,至少填有大部分数据,这时换另一种方法会更有效率。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="n">n</span> <span class="o">=</span> <span class="mi">16</span> <span class="n">myDict</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span> <span class="n">char</span> <span class="o">=</span> <span class="s">&#39;abcd&#39;</span><span class="p">[</span><span class="n">i</span><span class="o">%</span><span class="mi">4</span><span class="p">]</span> <span class="k">try</span><span class="p">:</span> <span class="n">myDict</span><span class="p">[</span><span class="n">char</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span> <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span> <span class="n">myDict</span><span class="p">[</span><span class="n">char</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">print</span><span class="p">(</span><span class="n">myDict</span><span class="p">)</span> </code></pre></div> <p>在两种方法中输出结果都是一样的。区别在于输出是如何获得的。跳出常规的思维模式,创建新的编程技巧能使你的应用更有效率。</p> <h2>窍门六:交叉编译你的应用</h2> <p>开发者有时会忘记计算机其实并不理解用来创建现代应用程序的编程语言。计算机理解的是机器语言。为了运行你的应用,你借助一个应用将你所编的人类可读的代码转换成机器可读的代码。有时,你用一种诸如Python这样的语言编写应用,再以C++这样的语言运行你的应用,这在运行的角度来说,是可行的。关键在于,你想你的应用完成什么事情,而你的主机系统能提供什么样的资源。</p> <p><a href="http://nuitka.net/">Nuitka</a>是一款有趣的交叉编译器,能将你的Python代码转化成C++代码。这样,你就可以在native模式下执行自己的应用,而无需依赖于解释器程序。你会发现自己的应用运行效率有了较大的提高,但是这会因平台和任务的差异而有所不同。</p> <p>(注意:Nuitka现在还处在测试阶段,所以在实际应用中请多加注意。实际上,当下最好还是把它用于实验。此外,关于交叉编译是否为提高运行效率的最佳方法还存在讨论的空间。开发者已经使用交叉编译多年,用来提高应用的速度。记住,每一种解决办法都有利有弊,在把它用于生产环境之前请仔细权衡。)</p> <p>在使用交叉编译器时,记得确保它支持你所用的Python版本。Nuitka支持Python2.6, 2.7, 3.2和3.3。为了让解决方案生效,你需要一个Python解释器和一个C++编译器。Nuitka支持许多C++编译器,其中包括<a href="http://www.visualstudio.com/">Microsoft Visual Studio</a>, <a href="http://www.mingw.org/">MinGW</a> 和 <a href="http://clang.llvm.org/">Clang/LLVM</a>。</p> <p>交叉编译可能造成一些严重问题。比如,在使用Nuitka时,你会发现即便是一个小程序也会消耗巨大的驱动空间。因为Nuitka借助一系列的动态链接库(DDLs)来执行Python的功能。因此,如果你用的是一个资源很有限的系统,这种方法或许不太可行。</p> <h2>结论</h2> <p>前文所述的六个窍门都能帮助你创建运行更有效率的Python应用。但是银弹是不存在的。上述的这些窍门不一定每次都能奏效。在特定的Python的版本下,有的窍门或许比其他的表现更好,但这有时候甚至取决于平台的差异。你需要总结分析你的应用,找到它效率低下的部分,然后尝试这些窍门,找到解决问题的最佳方法。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Ruby探针的基本实现原理 /ruby/2015/05/13/knowledge-behind-ruby-agent/ Wed, 13 May 2015 00:00:00 +0000 /ruby/2015/05/13/knowledge-behind-ruby-agent <h2>语言本身</h2> <p>Ruby语言支持语法级别的系统,框架,甚至语言本身的方法复写,一般叫做元编程(meta programming), 此基础之上还有一些术语为mixin,方法的动态定义,运行时类改写等等,这些技术和机制可以让语言本身就能实 现其他语言需要字节码才能实现的功能,例如探针需要hook <code>HttpRequest</code>中的<code>request</code>方法,就可以通过下面的方式实现:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">HttpRequest</span> <span class="k">def</span> <span class="nf">request_new</span> <span class="nb">puts</span> <span class="s1">&#39;before request&#39;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">request_old</span> <span class="nb">puts</span> <span class="s1">&#39;after request&#39;</span> <span class="n">result</span> <span class="k">end</span> <span class="n">alias_method</span> <span class="ss">:request_old</span><span class="p">,</span> <span class="ss">:request</span> <span class="n">alias_method</span> <span class="ss">:request</span><span class="p">,</span> <span class="ss">:request_new</span> <span class="k">end</span> </code></pre></div> <p>这里只是展示其中一种最简单的方法,还有很多其他方法,比如我们想完全放弃原有的方法的话,那就可以直接覆盖掉这个方法了。</p> <h2>框架</h2> <p>某些框架如Rails提供pub-sub机制,这种情况下探针只需要订阅特定类型的消息,然后进行数据再加工就可以了。对于Rails,这个比较简单:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">module</span> <span class="nn">ActionController</span> <span class="k">class</span> <span class="nc">PageRequest</span> <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">started</span><span class="p">,</span> <span class="n">finished</span><span class="p">,</span> <span class="n">unique_id</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span> <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">debug</span> <span class="o">[</span><span class="nb">name</span><span class="p">,</span> <span class="n">started</span><span class="p">,</span> <span class="n">finished</span><span class="p">,</span> <span class="n">unique_id</span><span class="p">,</span> <span class="n">payload</span><span class="o">].</span><span class="n">join</span><span class="p">(</span><span class="s1">&#39; &#39;</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Notifications</span><span class="o">.</span><span class="n">subscribe</span><span class="p">(</span><span class="s1">&#39;process_action.action_controller&#39;</span><span class="p">,</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">PageRequest</span><span class="o">.</span><span class="n">new</span><span class="p">)</span> <span class="c1"># 每次访问就能够订阅到‘process_action.action_controller’的消息:</span> <span class="c1">#</span> <span class="c1"># process_action.action_controller 2012-04-13 01:08:35 +0300 2012-04-13 01:08:35 +0300 af358ed7fab884532ec7 {</span> <span class="c1"># controller: &quot;Devise::SessionsController&quot;,</span> <span class="c1"># action: &quot;new&quot;,</span> <span class="c1"># params: {&quot;action&quot;=&gt;&quot;new&quot;, &quot;controller&quot;=&gt;&quot;devise/sessions&quot;},</span> <span class="c1"># format: :html,</span> <span class="c1"># method: &quot;GET&quot;,</span> <span class="c1"># path: &quot;/login/sign_in&quot;,</span> <span class="c1"># status: 200,</span> <span class="c1"># view_runtime: 279.3080806732178,</span> <span class="c1"># db_runtime: 40.053</span> <span class="c1"># }</span> </code></pre></div> <p>这样我们就能拿到这个<code>request</code>的数据了,订阅消息类型还请自己查阅相关框架的文档。</p> <h2>RACK</h2> <p>Rack绝对是个好东西,它把几乎所有的web框架和server沟通的接口定义好了,而且是如此的简洁,就是一个call方法。 一个最简单的Rack应用如下:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># configu.ru</span> <span class="k">class</span> <span class="nc">App</span> <span class="k">def</span> <span class="nf">call</span> <span class="o">[</span><span class="mi">200</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;Content-Type&quot;</span> <span class="o">=&gt;</span> <span class="s2">&quot;text/html&quot;</span><span class="p">},</span> <span class="o">[</span><span class="s2">&quot;hello world&quot;</span><span class="o">]]</span> <span class="k">end</span> <span class="k">end</span> <span class="n">run</span> <span class="n">app</span> <span class="c1"># rackup</span> </code></pre></div> <p>只需要这个call方法就搞定了,而多层的<code>rack middleware</code>也只是嵌套着调用<code>call</code>方法,所有要hook的话,这里就够了, 不过因为这里基本就是根了,需要注意抓取的数据量及堆栈信息,太长的话会影响性能的。</p> <h2>数据库</h2> <p>数据库层基本都是对orm框架的hook,在每个查询的结果输出中都会有执行的时间信息,超过阀值则调用数据库本身的查询 语句优化工具,并保存输出结果。至于hook的方法就是【1】中或者采用【2】中的方法,如果框架支持的话。</p> <h2>外部服务</h2> <p>外部服务就是对访问外部http请求的http_client类似的库进行hook,也就是按照【1】中的方法,对发起request的方法进行hook。</p> <h2>后台任务</h2> <p>后台任务机制同上面几项,只不过是在非http server的进程中运行,数据抓取的机制一样,但分类不同, 需要探针在运行过程中,判断数据抓取的对象状态,即web事务中还是非web事务中,非web事务都统一存 储到后台任务的数据容器中。</p> <h2>Thread Profiler</h2> <p>Thread Profiler就是一个加强版的事务采集器,可以设定采样周期和采样频率,将周期内的事务请求进行数据的聚合处理, 然后统计出在这个周期内的堆栈调用占比信息,然后可以根据此信息找出时间占比或者调用次数最多的方法,然后有针对性的 进行优化。</p> <h2>总结</h2> <p>以上所述,还只是一些最根本的原理性知识,细节的点还有很多,比如如何进行数据的本地存储和处理,以及线程间的数据冲突处理, 还有基于fork的多进程server的进程间的数据传输,在以后的文章中,会针对每一项都做出详细的解释,还请大家持续关注。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】JAVA 异常对于性能的影响 /pm/2015/05/12/Java-exception/ Tue, 12 May 2015 00:00:00 +0000 /pm/2015/05/12/Java-exception <div class="highlight"><pre><code class="language-text" data-lang="text">在对OneAPM的客户做技术支持时,我们常常会看到很多客户根本没意识到的异常。在消除了这些异常之后,代码运行速度与以前相比大幅提升。这让我们产生一种猜测,就是在代码里面使用异常会带来显著的性能开销。因为异常是错误情况处理的重要组成部分,摒弃是不太可能的,所以我们需要衡量异常处理对于性能影响,我们可以通过一个实验看看异常处理的对于性能的影响。 </code></pre></div> <h2>实验</h2> <p>我的实验基于一段随机抛出异常的简单代码。从科学的角度,这并非完全准确的测量,同时我也并不了解 HotSpot 编译器会对运行中的代码做何动作。但无论如何,这段代码应该能够让我们了解一些基本情况。</p> <p><img src="/images/Javaexception.png" alt="amzon-prof"></p> <p>结果很有意思:抛出与捕获异常的代价似乎极低。在我的例子里,大约是每个异常 0.02 毫秒。除非你真的抛出太多异常(我们指的是 10 万次或者更多),否则这一点基本都可忽略。 尽管这些结果显示出异常处理本身并不影响代码性能,但却并未解决下面这个问题:异常对性能的巨大影响该由谁负责?</p> <h3>我明显遗漏了什么重要的问题。</h3> <p>重新想了一下,我意识到自己遗漏了异常处理的一个重要部分。我没考虑到异常发生时你做了什么。在多数情况下你很有可能不仅仅是捕获异常!而问题就在这里:一般情况下,你会试图对问题进行补充,并让应用在最终用户那里仍能发挥功能。所以我遗漏的就是:“”为了处理异常而执行的补充代码“”。按照补充代码的不同,性能损失可能会变得相当显著。在某些情况下这可能意味着重试连接到服务器,在另一些情况下则可能意味着使用默认的回滚方案,而这种方案提供的解决办法肯定会带来非常差劲的性能。对于我们在很多情况下看到的行为,这似乎给出了很好的解释。</p> <p>不过我却不觉得分析到这里已经万事大吉,而是感到这里还遗漏了别的什么东西。</p> <h2>Stacktrace</h2> <p>对此问题,我仍颇为好奇,为此监视了收集 stacktrace 时情况性能有何变化。</p> <p>经常发生的情况应该是这样的:记下异常及其栈轨迹,尝试找出问题到底在哪。</p> <p>为此我修改了代码,额外收集了异常的 stacktrace 。这让情况显著改变。对异常的 stacktrace 的收集,其性能影响要比单纯捕获并抛出异常高出10倍。因此尽管 stacktrace 有助于理解哪里发生了问题(有可能还有助于理解为何发生问题),但却存在性能损失。 由于我们谈论的并非一条 stacktrace,所以此处的影响往往非常之大。 多数情况下,我们都要在多个层次上抛出并捕获异常。 我们看一个简单的例子: Web 服务客户端连接到服务器。首先,Java 库级别上存在一个连接失败异常。此后会有框架级别上的客户端失败异常,再以后可能还会有应用层次上的业务逻辑调用失败异常。到现在为止,总共要搜集三条 stacktrace。 多数情况下,你都能从日志文件或者应用输出中看到这些 stacktrace,而写入这些较长的 stacktrace 往往也会也带来性能影响。</p> <h2>结论</h2> <p>首先因为存在性能影响而把异常弃之不用并非良策。异常有助于提供一种一致的方式来解决运行时问题,并且有助于写出干净的代码。但我们应该对代码中抛出的异常数量进行跟踪,它们可能导致显著的性能影响。所以 OneAPM 默认要对所抛出的异常进行跟踪——在很多情况下人们都会对代码中发生的异常以及在解决这些异常时的性能损耗感到吃惊不已。 其次尽管使用异常很有裨益,您也应避免捕获过多的 strack trace。异常应该是为异常的情况而设计的,使用时应该牢记这一原则。当然,万一您不想遵从好的编程习惯,Java 语言就会让您知道,那样做可以让您的程序运行得更快,从而鼓励您去那样做。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Python Tricks 若干 /python/2015/04/29/python-tricks/ Wed, 29 Apr 2015 00:00:00 +0000 /python/2015/04/29/python-tricks <p>在 python 代码中可以看到一些常见的 trick,在这里做一个简单的小结。</p> <h3>json 字符串格式化</h3> <p>在开发 web 应用的时候经常会用到 json 字符串,但是一段比较长的 json 字符串是可读性较差的,不容易看出来里面结构的。 这时候就可以用 python 来把 json 字符串漂亮的打印出来。</p> <div class="highlight"><pre><code class="language-bash" data-lang="bash">root@Exp-1:/tmp# cat json.txt <span class="o">{</span><span class="s2">&quot;menu&quot;</span>: <span class="o">{</span><span class="s2">&quot;breakfast&quot;</span>: <span class="o">{</span><span class="s2">&quot;English Muffin&quot;</span>: <span class="o">{</span><span class="s2">&quot;price&quot;</span>: 7.5<span class="o">}</span>, <span class="s2">&quot;Bread Basket&quot;</span>: <span class="o">{</span><span class="s2">&quot;price&quot;</span>: 20, <span class="s2">&quot;desc&quot;</span>: <span class="s2">&quot;Assortment of fresh baked fruit breads and muffins&quot;</span><span class="o">}</span>, <span class="s2">&quot;Fruit Breads&quot;</span>: <span class="o">{</span><span class="s2">&quot;price&quot;</span>: 8<span class="o">}}</span>, <span class="s2">&quot;drink&quot;</span>: <span class="o">{</span><span class="s2">&quot;Hot Tea&quot;</span>: <span class="o">{</span><span class="s2">&quot;price&quot;</span>: 5<span class="o">}</span>, <span class="s2">&quot;Juice&quot;</span>: <span class="o">{</span><span class="s2">&quot;price&quot;</span>: 10, <span class="s2">&quot;type&quot;</span>: <span class="o">[</span><span class="s2">&quot;apple&quot;</span>, <span class="s2">&quot;watermelon&quot;</span>, <span class="s2">&quot;orange&quot;</span><span class="o">]}}}}</span> root@Exp-1:/tmp# root@Exp-1:/tmp# cat json.txt <span class="p">|</span> python -m json.tool <span class="o">{</span> <span class="s2">&quot;menu&quot;</span>: <span class="o">{</span> <span class="s2">&quot;breakfast&quot;</span>: <span class="o">{</span> <span class="s2">&quot;Bread Basket&quot;</span>: <span class="o">{</span> <span class="s2">&quot;desc&quot;</span>: <span class="s2">&quot;Assortment of fresh baked fruit breads and muffins&quot;</span>, <span class="s2">&quot;price&quot;</span>: 20 <span class="o">}</span>, <span class="s2">&quot;English Muffin&quot;</span>: <span class="o">{</span> <span class="s2">&quot;price&quot;</span>: 7.5 <span class="o">}</span>, <span class="s2">&quot;Fruit Breads&quot;</span>: <span class="o">{</span> <span class="s2">&quot;price&quot;</span>: 8 <span class="o">}</span> <span class="o">}</span>, <span class="s2">&quot;drink&quot;</span>: <span class="o">{</span> <span class="s2">&quot;Hot Tea&quot;</span>: <span class="o">{</span> <span class="s2">&quot;price&quot;</span>: 5 <span class="o">}</span>, <span class="s2">&quot;Juice&quot;</span>: <span class="o">{</span> <span class="s2">&quot;price&quot;</span>: 10, <span class="s2">&quot;type&quot;</span>: <span class="o">[</span> <span class="s2">&quot;apple&quot;</span>, <span class="s2">&quot;watermelon&quot;</span>, <span class="s2">&quot;orange&quot;</span> <span class="o">]</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> root@Exp-1:/tmp# </code></pre></div> <h3>else 的妙用</h3> <p>在某些场景下我们需要判断我们是否是从一个 for 循环中 break 跳出来的,并且只针对 break 跳出的情况做相应的处理。这时候我们通常的做法是使用一个 flag 变量来标识是否是从 for 循环中跳出的。 如下面的这个例子,查看在 60 到 80 之间是否存在 17 的倍数。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">flag</span> <span class="o">=</span> <span class="bp">False</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">60</span><span class="p">,</span> <span class="mi">80</span><span class="p">):</span> <span class="k">if</span> <span class="n">item</span> <span class="o">%</span> <span class="mi">17</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="n">flag</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">break</span> <span class="k">if</span> <span class="n">flag</span><span class="p">:</span> <span class="k">print</span> <span class="s">&quot;Exists at least one number can be divided by 17&quot;</span> </code></pre></div> <p>其实这时候可以使用 else 在不引入新变量的情况下达到同样的效果</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">60</span><span class="p">,</span> <span class="mi">80</span><span class="p">):</span> <span class="k">if</span> <span class="n">item</span> <span class="o">%</span> <span class="mi">17</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="n">flag</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">break</span> <span class="k">else</span><span class="p">:</span> <span class="k">print</span> <span class="s">&quot;exist&quot;</span> </code></pre></div> <h3>setdefault 方法</h3> <p>dictionary 是 python 一个很强大的内置数据结构,但是使用起来还是有不方便的地方,比如在多层嵌套的时候我们通常会这么写</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">dyna_routes</span> <span class="o">=</span> <span class="p">{}</span> <span class="n">method</span> <span class="o">=</span> <span class="s">&#39;GET&#39;</span> <span class="n">whole_rule</span> <span class="o">=</span> <span class="bp">None</span> <span class="c"># 一些其他的逻辑处理</span> <span class="o">...</span> <span class="k">if</span> <span class="n">method</span> <span class="ow">in</span> <span class="n">dyna_routes</span><span class="p">:</span> <span class="n">dyna_routes</span><span class="p">[</span><span class="n">method</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">whole_rule</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="n">dyna_routes</span><span class="p">[</span><span class="n">method</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">whole_rule</span><span class="p">]</span> </code></pre></div> <p>其实还有一种更简单的写法可以达到同样的效果</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="bp">self</span><span class="o">.</span><span class="n">dyna_routes</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="p">[])</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">whole_rule</span><span class="p">)</span> </code></pre></div> <p>或者可以使用 collections.defaultdict 模块</p> <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="nn">collections</span> <span class="n">dyna_routes</span> <span class="o">=</span> <span class="n">collections</span><span class="o">.</span><span class="n">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span> <span class="o">...</span> <span class="n">dyna_routes</span><span class="p">[</span><span class="n">method</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">whole_rule</span><span class="p">)</span> </code></pre></div> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Ruby中的语句中断和返回 /ruby/2015/04/28/ruby-return/ Tue, 28 Apr 2015 00:00:00 +0000 /ruby/2015/04/28/ruby-return <p><strong>return</strong>,<strong>break</strong>,<strong>next</strong> 这几个关键字的使用都涉及到跳出作用域的问题,而他们的不同 则在于不同的关键字跳出去的目的作用域的不同,因为有代码块则导致有一些地方需要格外注意。</p> <h2><em>return</em></h2> <h4>常用方式</h4> <p>通常情况下的<code>return</code>语句和大家理解的意思是相同的。</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="n">param</span> <span class="k">if</span> <span class="n">param</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">return</span> <span class="s1">&#39;returned 1&#39;</span> <span class="k">end</span> <span class="s1">&#39;returned default value&#39;</span> <span class="c1"># 根据Ruby语言规范,最后一条执行语句的结果将作为返回值返回,return是可选的</span> <span class="k">end</span> <span class="n">m1</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># =&gt; returned 1</span> <span class="n">m1</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># =&gt; returned default value</span> </code></pre></div> <p>在有异常捕获的<code>ensure</code>时,情况会稍有不同:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="s1">&#39;return default&#39;</span> <span class="k">ensure</span> <span class="nb">puts</span> <span class="s1">&#39;I am sure that it will be here!&#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># =&gt; I am sure that it will be here!</span> </code></pre></div> <p>像这种情况,在<code>ensure</code>语句之前,无论是否显示用<code>return</code>来返回,<code>m1</code>方法都会返回<code>ensure</code>之前的值, <code>ensure</code>语句只是确保之后的代码块<code>puts &#39;I am sure that it will be here!&#39;</code>执行,但是不会从这里返回。 如果在<code>ensure</code>语句中显示的用<code>return</code>来返回值时,情况就不一样了。示例如下:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="k">return</span> <span class="s1">&#39;return default&#39;</span> <span class="k">ensure</span> <span class="k">return</span> <span class="s1">&#39;I am sure that it will be here!&#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># =&gt; I am sure that it will be here!</span> </code></pre></div> <p>无论在<code>ensure</code>之前是否显示返回,都只会返回<code>ensure</code>之后的值。</p> <p>在有代码块干预的情况下,又会有所不同:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="nb">p</span> <span class="s1">&#39;start ... &#39;</span> <span class="nb">proc</span> <span class="k">do</span> <span class="nb">p</span> <span class="s1">&#39;block start&#39;</span> <span class="k">return</span> <span class="nb">p</span> <span class="s1">&#39;block end&#39;</span> <span class="k">end</span><span class="o">.</span><span class="n">call</span> <span class="nb">p</span> <span class="s1">&#39;end ... &#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># 输出结果:</span> <span class="c1">#</span> <span class="c1"># &quot;start ... &quot;</span> <span class="c1"># &quot;block start&quot;</span> </code></pre></div> <p>这个应该是在预料之中的,再看下一个:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="nb">p</span> <span class="s1">&#39;start ... &#39;</span> <span class="o">-&gt;</span> <span class="k">do</span> <span class="nb">p</span> <span class="s1">&#39;block start&#39;</span> <span class="k">return</span> <span class="nb">p</span> <span class="s1">&#39;block end&#39;</span> <span class="k">end</span><span class="o">.</span><span class="n">call</span> <span class="nb">p</span> <span class="s1">&#39;end ... &#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># 输出结果:</span> <span class="c1">#</span> <span class="c1"># &quot;start ... &quot;</span> <span class="c1"># &quot;block start&quot;</span> <span class="c1"># &quot;end ... &quot;</span> </code></pre></div> <p>这里多了一行<code>&quot;end ... &quot;</code>,原因何在?这就是<code>Proc</code>和<code>Lambda</code>最大的区别,在他们之中的<code>return</code> 语句跳出去的目的作用域不同,<code>Proc</code>会直接跳出整个方法的调用,而<code>Lambda</code>只会跳出自身的作用域, 返回到方法中继续执行,这一点需要格外注意。(在<code>break</code>中,<code>Proc</code>和<code>Lambda</code>的跳出方式和<code>return</code>是一样的,后面就不再赘述了。)</p> <h2><em>break</em></h2> <p>先来看一个简单的小例子:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">result</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="o">].</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">2</span> <span class="k">end</span> <span class="nb">p</span> <span class="n">result</span> <span class="c1"># =&gt; [2, 4, 6, 8, 10]</span> </code></pre></div> <p>这个没什么奇怪的,那么看看下面这个,来猜猜它的输出结果是什么?</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">result</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="o">].</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="k">break</span> <span class="k">if</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">2</span> <span class="k">end</span> <span class="c1"># FLAG</span> <span class="nb">p</span> <span class="n">result</span> </code></pre></div> <p>是<code>[1, 2, 3, nil, nil]</code>?还是<code>[1, 2, 3]</code>?还是什么?答案是<code>nil</code>,因为执行<code>break</code>后,直接跳到了<em><code>FLAG</code></em> ,也就是跳出了<code>map</code>方法,而<code>map</code>方法中的语句并没有执行完,导致没有任何返回值,为了验证这个想法是正确的,我们 可以利用Ruby语言的<code>break</code>可以带返回值的特性来验证一下:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">result</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="o">].</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="k">break</span> <span class="s1">&#39;returned break&#39;</span> <span class="k">if</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">2</span> <span class="k">end</span> <span class="nb">p</span> <span class="n">result</span> <span class="c1"># =&gt; &quot;returned break&quot;</span> </code></pre></div> <p>这里可以证明我们的猜测是正确的。虽然上面说明了这个问题,但是应该还不是非常容易理解,我们自己定义 一个代码块,再来说明一下:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="nb">p</span> <span class="s1">&#39;start in m1 ... &#39;</span> <span class="n">m2</span> <span class="k">do</span> <span class="c1"># 代码块</span> <span class="nb">p</span> <span class="s1">&#39;start in block in m1 ... &#39;</span> <span class="nb">p</span> <span class="s1">&#39;end in block in m1 ... &#39;</span> <span class="k">end</span> <span class="nb">p</span> <span class="s1">&#39;end in m1 ... &#39;</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">m2</span> <span class="o">&amp;</span><span class="n">block</span> <span class="nb">p</span> <span class="s1">&#39;start in m2 ... &#39;</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span> <span class="nb">p</span> <span class="s1">&#39;end in m2 ... &#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># 输出结果:</span> <span class="c1">#</span> <span class="c1"># &quot;start in m1 ... &quot;</span> <span class="c1"># &quot;start in m2 ... &quot;</span> <span class="c1"># &quot;start in block in m1 ... &quot;</span> <span class="c1"># &quot;end in block in m1 ... &quot;</span> <span class="c1"># &quot;end in m2 ... &quot;</span> <span class="c1"># &quot;end in m1 ... &quot;</span> </code></pre></div> <p>然后我们在<code>m1</code>中的<code>block</code>中添加<code>break</code>,来看看执行结果:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="nb">p</span> <span class="s1">&#39;start in m1 ... &#39;</span> <span class="n">m2</span> <span class="k">do</span> <span class="c1"># 代码块</span> <span class="nb">p</span> <span class="s1">&#39;start in block in m1 ... &#39;</span> <span class="k">break</span> <span class="nb">p</span> <span class="s1">&#39;end in block in m1 ... &#39;</span> <span class="k">end</span> <span class="nb">p</span> <span class="s1">&#39;end in m1 ... &#39;</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">m2</span> <span class="o">&amp;</span><span class="n">block</span> <span class="nb">p</span> <span class="s1">&#39;start in m2 ... &#39;</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span> <span class="nb">p</span> <span class="s1">&#39;end in m2 ... &#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># 输出结果:</span> <span class="c1">#</span> <span class="c1"># &quot;start in m1 ... &quot;</span> <span class="c1"># &quot;start in m2 ... &quot;</span> <span class="c1"># &quot;start in block in m1 ... &quot;</span> <span class="c1"># &quot;end in m1 ... &quot;</span> </code></pre></div> <p>可以看到代码块的最后一行代码没有执行,<code>m2</code>的最后一行也没有执行,就是因为这一行没有执行,导致 <code>break</code>的第二个例子中的<code>map</code>没有返回任何值。总结一下,代码块中的<code>break</code>会直接跳出调用的方法(m2), 而在声明代码块的方法(m1)中继续执行此方法(m1)中剩下的语句。</p> <h2><em>next</em></h2> <p><code>next</code>关键字类似其他语言中的<code>continue</code>,它的工作方式基本和<code>continue</code>类似。</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="nb">p</span> <span class="s1">&#39;start in m1 ... &#39;</span> <span class="n">m2</span> <span class="k">do</span> <span class="c1"># 代码块</span> <span class="nb">p</span> <span class="s1">&#39;start in block in m1 ... &#39;</span> <span class="k">next</span> <span class="nb">p</span> <span class="s1">&#39;end in block in m1 ... &#39;</span> <span class="k">end</span> <span class="nb">p</span> <span class="s1">&#39;end in m1 ... &#39;</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">m2</span> <span class="o">&amp;</span><span class="n">block</span> <span class="nb">p</span> <span class="s1">&#39;start in m2 ... &#39;</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span> <span class="nb">p</span> <span class="s1">&#39;end in m2 ... &#39;</span> <span class="k">end</span> <span class="n">m1</span> <span class="c1"># 输出结果:</span> <span class="c1">#</span> <span class="c1"># &quot;start in m1 ... &quot;</span> <span class="c1"># &quot;start in m2 ... &quot;</span> <span class="c1"># &quot;start in block in m1 ... &quot;</span> <span class="c1"># &quot;end in m2 ... &quot;</span> <span class="c1"># &quot;end in m1 ... &quot;</span> </code></pre></div> <p>只是略过了代码块的最后一行代码,这就是<code>next</code>的工作方式了。我们再来看看<code>break</code>的那个例子如果 用<code>next</code>来写,看看结果是什么?如果你完全理解了上面所写的,相信你已经能在大脑中计算出结果了:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">result</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="o">].</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="k">next</span> <span class="k">if</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">2</span> <span class="k">end</span> <span class="nb">p</span> <span class="n">result</span> <span class="c1"># =&gt; [2, 4, 6, nil, nil]</span> </code></pre></div> <p><code>next</code>语句也能带返回值:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">result</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="o">].</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="k">next</span> <span class="s1">&#39;next&#39;</span> <span class="k">if</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">2</span> <span class="k">end</span> <span class="nb">p</span> <span class="n">result</span> <span class="c1"># =&gt; [2, 4, 6, &quot;next&quot;, &quot;next&quot;]</span> </code></pre></div> <h2><em>其他</em></h2> <p>对于<code>return</code>,在方法中,代码块中都可以使用,而<code>break</code>和<code>next</code>只能在代码块中使用(循环结构中 也可以使用,但是一般它也是用代码块的形式来表示),如果在方法中调用两者会提示语法错误,也就是:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">m1</span> <span class="k">return</span> <span class="c1"># OK</span> <span class="k">break</span> <span class="c1"># Invalid break, compile error (SyntaxError)</span> <span class="k">next</span> <span class="c1"># Invalid next, compile error (SyntaxError)</span> <span class="k">end</span> </code></pre></div> <h2><em>结论</em></h2> <p><strong>return</strong> 大部分情况下和其他语言无异,需要注意在<code>ensure</code>以及<code>Proc</code>和<code>Lambda</code>两种不同的 代码块中的细节问题。</p> <p><strong>break</strong> 在有方法嵌套调用中的代码块中需要注意,它总是返回到调用代码块方法的方法中(有点绕)。</p> <p><strong>next</strong> 最老实,基本不需要注意什么。</p> <p>最后就是,不只是<code>return</code>能返回值,<code>break</code>和<code>next</code>都能返回值。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Python - 装饰器使用过程中的误区 /python/2015/04/27/python-decorator-mistake/ Mon, 27 Apr 2015 00:00:00 +0000 /python/2015/04/27/python-decorator-mistake <h2><strong>装饰器基本概念</strong></h2> <p>大家都知道装饰器是一个很著名的设计模式,经常被用于AOP(面向切面编程)的场景,较为经典的有插入日志,性能测试,事务处理,Web权限校验,Cache等。</p> <p>Python语言本身提供了装饰器语法(@),典型的装饰器实现如下:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="nd">@function_wrapper</span> <span class="k">def</span> <span class="nf">function</span><span class="p">():</span> <span class="k">pass</span> </code></pre></div> <p>@实际上是python2.4才提出的语法糖,针对python2.4以前的版本有另一种等价的实现:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="k">def</span> <span class="nf">function</span><span class="p">():</span> <span class="k">pass</span> <span class="n">function</span> <span class="o">=</span> <span class="n">function_wrapper</span><span class="p">(</span><span class="n">function</span><span class="p">)</span> </code></pre></div> <h2><strong>装饰器的两种实现</strong></h2> <p><strong>函数包装器 - 经典实现</strong></p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="k">def</span> <span class="nf">function_wrapper</span><span class="p">(</span><span class="n">wrapped</span><span class="p">):</span> <span class="k">def</span> <span class="nf">_wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="k">return</span> <span class="n">wrapped</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="k">return</span> <span class="n">_wrapper</span> <span class="nd">@function_wrapper</span> <span class="k">def</span> <span class="nf">function</span><span class="p">():</span> <span class="k">pass</span> </code></pre></div> <p><strong>类包装器 - 易于理解</strong></p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="k">class</span> <span class="nc">function_wrapper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">wrapped</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">wrapped</span> <span class="o">=</span> <span class="n">wrapped</span> <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">wrapped</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="nd">@function_wrapper</span> <span class="k">def</span> <span class="nf">function</span><span class="p">():</span> <span class="k">pass</span> </code></pre></div> <h2><strong>函数(function)自省</strong></h2> <p>当我们谈到一个函数时,通常希望这个函数的属性像其文档上描述的那样,是被明确定义的,例如<code>__name__</code>和<code>__doc__</code> 。</p> <p>针对某个函数应用装饰器时,这个函数的属性就会发生变化,但这并不是我们所期望的。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="k">def</span> <span class="nf">function_wrapper</span><span class="p">(</span><span class="n">wrapped</span><span class="p">):</span> <span class="k">def</span> <span class="nf">_wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="k">return</span> <span class="n">wrapped</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="k">return</span> <span class="n">_wrapper</span> <span class="nd">@function_wrapper</span> <span class="k">def</span> <span class="nf">function</span><span class="p">():</span> <span class="k">pass</span> <span class="o">&gt;&gt;&gt;</span> <span class="k">print</span><span class="p">(</span><span class="n">function</span><span class="o">.</span><span class="n">__name__</span><span class="p">)</span> <span class="n">_wrapper</span> </code></pre></div> <p>python标准库提供了<code>functools.wraps()</code>,来解决这个问题。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="kn">import</span> <span class="nn">functools</span> <span class="k">def</span> <span class="nf">function_wrapper</span><span class="p">(</span><span class="n">wrapped</span><span class="p">):</span> <span class="nd">@functools.wraps</span><span class="p">(</span><span class="n">wrapped</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="k">return</span> <span class="n">wrapped</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="k">return</span> <span class="n">_wrapper</span> <span class="nd">@function_wrapper</span> <span class="k">def</span> <span class="nf">function</span><span class="p">():</span> <span class="k">pass</span> <span class="o">&gt;&gt;&gt;</span> <span class="k">print</span><span class="p">(</span><span class="n">function</span><span class="o">.</span><span class="n">__name__</span><span class="p">)</span> <span class="n">function</span> </code></pre></div> <p>然而,当我们想要获取被包装函数的参数(<code>argument</code>)或源代码(<code>source code</code>)时,同样不能得到我们想要的结果。</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="kn">import</span> <span class="nn">inspect</span> <span class="k">def</span> <span class="nf">function_wrapper</span><span class="p">(</span><span class="n">wrapped</span><span class="p">):</span> <span class="o">...</span> <span class="nd">@function_wrapper</span> <span class="k">def</span> <span class="nf">function</span><span class="p">(</span><span class="n">arg1</span><span class="p">,</span> <span class="n">arg2</span><span class="p">):</span> <span class="k">pass</span> <span class="o">&gt;&gt;&gt;</span> <span class="k">print</span><span class="p">(</span><span class="n">inspect</span><span class="o">.</span><span class="n">getargspec</span><span class="p">(</span><span class="n">function</span><span class="p">))</span> <span class="n">ArgSpec</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="p">[],</span> <span class="n">varargs</span><span class="o">=</span><span class="s">&#39;args&#39;</span><span class="p">,</span> <span class="n">keywords</span><span class="o">=</span><span class="s">&#39;kwargs&#39;</span><span class="p">,</span> <span class="n">defaults</span><span class="o">=</span><span class="bp">None</span><span class="p">)</span> <span class="o">&gt;&gt;&gt;</span> <span class="k">print</span><span class="p">(</span><span class="n">inspect</span><span class="o">.</span><span class="n">getsource</span><span class="p">(</span><span class="n">function</span><span class="p">))</span> <span class="nd">@functools.wraps</span><span class="p">(</span><span class="n">wrapped</span><span class="p">)</span> <span class="k">def</span> <span class="nf">_wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="k">return</span> <span class="n">wrapped</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> </code></pre></div> <h2><strong>包装类方法(<code>@classmethod</code>)</strong></h2> <p>当包装器(<code>@function_wrapper</code>)被应用于<code>@classmethod</code>时,将会抛出如下异常:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="k">class</span> <span class="nc">Class</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="nd">@function_wrapper</span> <span class="nd">@classmethod</span> <span class="k">def</span> <span class="nf">cmethod</span><span class="p">(</span><span class="n">cls</span><span class="p">):</span> <span class="k">pass</span> <span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span> <span class="n">File</span> <span class="s">&quot;&lt;stdin&gt;&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">1</span><span class="p">,</span> <span class="ow">in</span> <span class="o">&lt;</span><span class="n">module</span><span class="o">&gt;</span> <span class="n">File</span> <span class="s">&quot;&lt;stdin&gt;&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">3</span><span class="p">,</span> <span class="ow">in</span> <span class="n">Class</span> <span class="n">File</span> <span class="s">&quot;&lt;stdin&gt;&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">2</span><span class="p">,</span> <span class="ow">in</span> <span class="n">wrapper</span> <span class="n">File</span> <span class="s">&quot;.../functools.py&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">33</span><span class="p">,</span> <span class="ow">in</span> <span class="n">update_wrapper</span> <span class="nb">setattr</span><span class="p">(</span><span class="n">wrapper</span><span class="p">,</span> <span class="n">attr</span><span class="p">,</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">wrapped</span><span class="p">,</span> <span class="n">attr</span><span class="p">))</span> <span class="ne">AttributeError</span><span class="p">:</span> <span class="s">&#39;classmethod&#39;</span> <span class="nb">object</span> <span class="n">has</span> <span class="n">no</span> <span class="n">attribute</span> <span class="s">&#39;__module__&#39;</span> </code></pre></div> <p>因为<code>@classmethod</code>在实现时,缺少<code>functools.update_wrapper</code>需要的某些属性。这是<code>functools.update_wrapper</code>在python2中的bug,3.2版本已被修复,参考<a href="http://bugs.python.org/issue3445">http://bugs.python.org/issue3445</a>。</p> <p>然而,在python3下执行,另一个问题出现了:</p> <div class="highlight"><pre><code class="language-python" data-lang="python"> <span class="k">class</span> <span class="nc">Class</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="nd">@function_wrapper</span> <span class="nd">@classmethod</span> <span class="k">def</span> <span class="nf">cmethod</span><span class="p">(</span><span class="n">cls</span><span class="p">):</span> <span class="k">pass</span> <span class="o">&gt;&gt;&gt;</span> <span class="n">Class</span><span class="o">.</span><span class="n">cmethod</span><span class="p">()</span> <span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span> <span class="n">File</span> <span class="s">&quot;classmethod.py&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">15</span><span class="p">,</span> <span class="ow">in</span> <span class="o">&lt;</span><span class="n">module</span><span class="o">&gt;</span> <span class="n">Class</span><span class="o">.</span><span class="n">cmethod</span><span class="p">()</span> <span class="n">File</span> <span class="s">&quot;classmethod.py&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">6</span><span class="p">,</span> <span class="ow">in</span> <span class="n">_wrapper</span> <span class="k">return</span> <span class="n">wrapped</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="ne">TypeError</span><span class="p">:</span> <span class="s">&#39;classmethod&#39;</span> <span class="nb">object</span> <span class="ow">is</span> <span class="ow">not</span> <span class="nb">callable</span> </code></pre></div> <p>这是因为包装器认定被包装的函数(<code>@classmethod</code>)是可以直接被调用的,但事实并不一定是这样的。被包装的函数实际上可能是描述符(<code>descriptor</code>),意味着为了使其可调用,该函数(描述符)必须被正确地绑定到某个实例上。关于描述符的定义,可以参考<a href="https://docs.python.org/2/howto/descriptor.html">https://docs.python.org/2/howto/descriptor.html</a>。</p> <h2><strong>总结 - 简单并不意味着正确</strong></h2> <p>尽管大家实现装饰器所用的方法通常都很简单,但这并不意味着它们一定是正确的并且始终能正常工作。</p> <p>如同上面我们所看到的,<code>functools.wraps()</code>可以帮我们解决<code>__name__</code>和<code>__doc__</code> 的问题,但对于获取函数的参数(<code>argument</code>)或源代码(<code>source code</code>)则束手无策。</p> <p>以上问题,<a href="https://github.com/GrahamDumpleton/wrapt">wrapt</a>都可以帮忙解决,详细用法可参考其官方文档:<a href="http://wrapt.readthedocs.org">http://wrapt.readthedocs.org</a></p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Session Trace 功能说明,欢迎吐槽 /javascript/2015/04/16/helloword/ Thu, 16 Apr 2015 00:00:00 +0000 /javascript/2015/04/16/helloword <p>为什么要做Session Trace 这个功能? 在页面加载 DOM processing 和Page rendering 两个阶段耗时比较长,但之前的Browser Insight 没有展示这两个阶段细节。<br> <img src="/images/session/why.jpg" alt="ruby-prof"></p> <h2>session Trace 源自 chrome developer tools</h2> <p>现在Browser Insight 可以收集到从用户点击页面页面加载,到用户离开页面页面卸载这个过程中的全部资源的加载时间、ajax请求时间、事件的触发时间、和部分函数执行的时间<br></p> <p>先看看正在做的截图吧 对于资源的加载有dns 解析等时间 <img src="/images/session/session.jpg" alt="ruby-prof"> 对于ajax 有等待时间和回调函数执行的时间<br> <img src="/images/session/ajax.png" alt="ruby-prof"> 之前在trace 详情里面,无法查看细节的 dom和page,也可以在这里找到对应的详情,<br> <img src="/images/session/trace.jpg" alt="ruby-prof"> 菜单栏里面的按钮 dom 和page 分别可以查看 在dom构建和page渲染两个阶段的详细情况<br> <img src="/images/session/dominfo.jpg" alt="ruby-prof"> 页面中加载这么多资源和发起这个多ajax,耗时top5资源和ajax已经找出来了,需要注意的是,这里的top5 是用户在整个页面进行所有操作后,页面所加载的资源和发起的ajax,而不仅仅是页面加载阶段的。<br> <img src="/images/session/top.jpg" alt="ruby-prof"> 用户的浏览器信息、地理位置信息、页面加载阶段的DOM processing 耗时和 Page rendering 耗时 和错误的数量 都放在一起展示了<br> <img src="/images/session/head.jpg" alt="ruby-prof"></p> <h2>时间坐标、时间戳、时间段</h2> <p><img src="/images/session/time.jpg" alt="ruby-prof"> 1 是坐标 会随着鼠标的滚动而变化,不是固定死的,<br> 2 时间戳 一些事件的触发和执行时间小于1ms 的事件被作为时间戳,用一个小圆点表示<br> 3 资源和ajax和耗时较长的事件都被作为时间段,用一个长方形表示<br></p> <h2>Session Trace 下一步解决问题</h2> <p>重点解决交互过程中的关键操作的耗时 <img src="/images/session/jiaohu.jpg" alt="ruby-prof"></p> <h2>Session Trace 将于近期上线,欢迎再评论里面反馈问题和bug</h2> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> Ruby DATA /ruby/2015/04/14/ruby-data/ Tue, 14 Apr 2015 00:00:00 +0000 /ruby/2015/04/14/ruby-data <p>这段代码能运行吗? 这个DATA是什么东西?</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">&#39;erb&#39;</span> <span class="n">data</span> <span class="o">=</span> <span class="no">DATA</span><span class="o">.</span><span class="n">read</span> <span class="n">max</span> <span class="o">=</span> <span class="mi">15_000</span> <span class="n">title</span> <span class="o">=</span> <span class="s2">&quot;hello world!&quot;</span> <span class="n">content</span> <span class="o">=</span> <span class="s2">&quot;hello world!</span><span class="se">\n</span><span class="s2">&quot;</span> <span class="o">*</span> <span class="mi">10</span> <span class="n">max</span><span class="o">.</span><span class="n">times</span><span class="p">{</span> <span class="no">ERB</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">result</span><span class="p">(</span><span class="nb">binding</span><span class="p">)</span> <span class="p">}</span> <span class="cp">__END__</span> <span class="cp">&lt;html&gt;</span> <span class="cp"> &lt;head&gt; &lt;%= title %&gt; &lt;/head&gt;</span> <span class="cp"> &lt;body&gt;</span> <span class="cp"> &lt;h1&gt; &lt;%= title %&gt; &lt;/h1&gt;</span> <span class="cp"> &lt;p&gt;</span> <span class="cp"> &lt;%= content %&gt;</span> <span class="cp"> &lt;/p&gt;</span> <span class="cp"> &lt;/body&gt;</span> <span class="cp">&lt;/html&gt;</span> </code></pre></div> <p>这个神奇的DATA是一个IO对象,读取<code>__END__</code>之后内容。 有一点需要注意的是<code>DATA.read</code>会将<code>__END__</code>之后 的内容一次性读出,由于IO读取的特性,当第 二次<code>DATA.read</code>的时候内容就会为空,如果需要第二次读取,那 么先要执行<code>DATA.rewind</code>。</p> <p>有的时候我们写一个脚本来做一些自动化的工作,需要预先读取一个文件的内容,作为输入或者作为模版,我们可 以先把这部分内容附到<code>__END__</code>后,然后用<code>DATA</code>来读取,因为它是标准的IO对象,我们可以像处理普通 文件一样处理<code>__END__</code>后的内容,如上面的代码所示,用来存储<code>ERB</code>内容作为模版,然后再进行处理,非常的 方便。</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 【译】NodeJS错误处理最佳实践 /nodejs/2015/04/13/nodejs-errorhandling/ Mon, 13 Apr 2015 00:00:00 +0000 /nodejs/2015/04/13/nodejs-errorhandling <blockquote> <p>NodeJS的错误处理让人痛苦,在很长的一段时间里,大量的错误被放任不管。但是要想建立一个健壮的Node.js程序就必须正确的处理这些错误,而且这并不难学。如果你实在没有耐心,那就直接绕过长篇大论跳到“总结”部分吧。</p> </blockquote> <p><a href="https://www.joyent.com/developers/node/design/errors">原文</a></p> <p>这篇文章会回答NodeJS初学者的若干问题:</p> <ul> <li>我写的函数里什么时候该抛出异常,什么时候该传给callback, 什么时候触发<code>EventEmitter</code>等等。</li> <li>我的函数对参数该做出怎样的假设?我应该检查更加具体的约束么?例如参数是否非空,是否大于零,是不是看起来像个IP地址,等等等。</li> <li>我该如何处理那些不符合预期的参数?我是应该抛出一个异常,还是把错误传递给一个callback。</li> <li>我该怎么在程序里区分不同的异常(比如“请求错误”和“服务不可用”)?</li> <li>我怎么才能提供足够的信息让调用者知晓错误细节。</li> <li>我该怎么处理未预料的出错?我是应该用 <code>try/catch</code> ,<code>domains</code> 还是其它什么方式呢?</li> </ul> <p>这篇文章可以划分成互相为基础的几个部分:</p> <ul> <li><strong>背景</strong>:希望你所具备的知识。</li> <li><strong>操作失败和程序员的失误</strong>:介绍两种基本的异常。</li> <li><strong>编写新函数的实践</strong>:关于怎么让函数产生有用报错的基本原则。</li> <li><strong>编写新函数的具体推荐</strong>:编写能产生有用报错的、健壮的函数需要的一个检查列表</li> <li><strong>例子</strong>:以<code>connect</code>函数为例的文档和序言。</li> <li><strong>总结</strong>:全文至此的观点总结。</li> <li><strong>附录:Error对象属性约定</strong>:用标准方式提供一个属性列表,以提供更多信息。</li> </ul> <h2>背景</h2> <p>本文假设:</p> <ul> <li>你已经熟悉了JavaScript、Java、 Python、 C++ 或者类似的语言中异常的概念,而且你知道抛出异常和捕获异常是什么意思。</li> <li>你熟悉怎么用NodeJS编写代码。你使用异步操作的时候会很自在,并能用<code>callback(err,result)</code>模式去完成异步操作。你得知道下面的代码不能正确处理异常的原因是什么[脚注1]</li> </ul> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">function</span> <span class="nx">myApiFunc</span><span class="p">(</span><span class="nx">callback</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/*</span> <span class="cm"> * This pattern does NOT work!</span> <span class="cm"> */</span> <span class="k">try</span> <span class="p">{</span> <span class="nx">doSomeAsynchronousOperation</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">throw</span> <span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="cm">/* continue as normal */</span> <span class="p">});</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">ex</span><span class="p">)</span> <span class="p">{</span> <span class="nx">callback</span><span class="p">(</span><span class="nx">ex</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div> <p>你还要熟悉三种传递错误的方式: - 作为异常抛出。 - 把错误传给一个callback,这个函数正是为了处理异常和处理异步操作返回结果的。 - 在EventEmitter上触发一个Error事件。</p> <p>接下来我们会详细讨论这几种方式。这篇文章不假设你知道任何关于domains的知识。</p> <p>最后,你应该知道在JavaScript里,错误和异常是有区别的。错误是Error的一个实例。错误被创建并且直接传递给另一个函数或者被抛出。如果一个错误被抛出了那么它就变成了一个异常[脚注2]。举个例子:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;something bad happened&#39;</span><span class="p">);</span> </code></pre></div> <p>但是使用一个错误而不抛出也是可以的</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">callback</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;something bad happened&#39;</span><span class="p">));</span> </code></pre></div> <p>这种用法更常见,因为在NodeJS里,大部分的错误都是异步的。实际上,<code>try/catch</code>唯一常用的是在<code>JSON.parse</code>和类似验证用户输入的地方。接下来我们会看到,其实很少要捕获一个异步函数里的异常。这一点和Java,C++,以及其它严重依赖异常的语言很不一样。</p> <h2>操作失败和程序员的失误</h2> <p>把错误分成两大类很有用[脚注3]:</p> <ul> <li><strong>操作失败</strong> 是正确编写的程序在运行时产生的错误。它并不是程序的Bug,反而经常是其它问题:系统本身(内存不足或者打开文件数过多),系统配置(没有到达远程主机的路由),网络问题(端口挂起),远程服务(500错误,连接失败)。例子如下:</li> <li>连接不到服务器</li> <li>无法解析主机名</li> <li>无效的用户输入</li> <li>请求超时</li> <li>服务器返回500</li> <li>套接字被挂起</li> <li><p>系统内存不足</p></li> <li><p><strong>程序员失误</strong> 是程序里的Bug。这些错误往往可以通过修改代码避免。它们永远都没法被有效的处理。</p></li> <li><p>读取 undefined 的一个属性</p></li> <li><p>调用异步函数没有指定回调</p></li> <li><p>该传对象的时候传了一个字符串</p></li> <li><p>该传IP地址的时候传了一个对象</p></li> </ul> <p>人们把操作失败和程序员的失误都称为“错误”,但其实它们很不一样。操作失败是所有正确的程序应该处理的错误情形,只要被妥善处理它们不一定会预示着Bug或是严重的问题。“文件找不到”是一个操作失败,但是它并不一定意味着哪里出错了。它可能只是代表着程序如果想用一个文件得事先创建它。</p> <p>与之相反,程序员失误是彻彻底底的Bug。这些情形下你会犯错:忘记验证用户输入,敲错了变量名,诸如此类。这样的错误根本就没法被处理,如果可以,那就意味着你用处理错误的代码代替了出错的代码。</p> <p>这样的区分很重要:操作失败是程序正常操作的一部分。而由程序员的失误则是Bug。</p> <p>有的时候,你会在一个Root问题里同时遇到操作失败和程序员的失误。HTTP服务器访问了未定义的变量时奔溃了,这是程序员的失误。当前连接着的客户端会在程序崩溃的同时看到一个<code>ECONNRESET</code>错误,在NodeJS里通常会被报成“Socket Hang-up”。对客户端来说,这是一个不相关的操作失败, 那是因为正确的客户端必须处理服务器宕机或者网络中断的情况。</p> <p>类似的,如果不处理好操作失败, 这本身就是一个失误。举个例子,如果程序想要连接服务器,但是得到一个<code>ECONNREFUSED</code>错误,而这个程序没有监听套接字上的 <code>error</code>事件,然后程序崩溃了,这是程序员的失误。连接断开是操作失败(因为这是任何一个正确的程序在系统的网络或者其它模块出问题时都会经历的),如果它不被正确处理,那它就是一个失误。</p> <p>理解操作失败和程序员失误的不同, 是搞清怎么传递异常和处理异常的基础。明白了这点再继续往下读。</p> <h3>处理操作失败</h3> <p>就像性能和安全问题一样,错误处理并不是可以凭空加到一个没有任何错误处理的程序中的。你没有办法在一个集中的地方处理所有的异常,就像你不能在一个集中的地方解决所有的性能问题。你得考虑任何会导致失败的代码(比如打开文件,连接服务器,Fork子进程等)可能产生的结果。包括为什么出错,错误背后的原因。之后会提及,但是关键在于错误处理的粒度要细,因为哪里出错和为什么出错决定了影响大小和对策。</p> <p>你可能会发现在栈的某几层不断地处理相同的错误。这是因为底层除了向上层传递错误,上层再向它的上层传递错误以外,底层没有做任何有意义的事情。通常,只有顶层的调用者知道正确的应对是什么,是重试操作,报告给用户还是其它。但是那并不意味着,你应该把所有的错误全都丢给顶层的回调函数。因为,顶层的回调函数不知道发生错误的上下文,不知道哪些操作已经成功执行,哪些操作实际上失败了。</p> <p>我们来更具体一些。对于一个给定的错误,你可以做这些事情:</p> <ul> <li><p><strong>直接处理</strong>。有的时候该做什么很清楚。如果你在尝试打开日志文件的时候得到了一个<code>ENOENT</code>错误,很有可能你是第一次打开这个文件,你要做的就是首先创建它。更有意思的例子是,你维护着到服务器(比如数据库)的持久连接,然后遇到了一个“socket hang-up”的异常。这通常意味着要么远端要么本地的网络失败了。很多时候这种错误是暂时的,所以大部分情况下你得重新连接来解决问题。(这和接下来的重试不大一样,因为在你得到这个错误的时候不一定有操作正在进行)</p></li> <li><p><strong>把出错扩散到客户端</strong>。如果你不知道怎么处理这个异常,最简单的方式就是放弃你正在执行的操作,清理所有开始的,然后把错误传递给客户端。(怎么传递异常是另外一回事了,接下来会讨论)。这种方式适合错误短时间内无法解决的情形。比如,用户提交了不正确的JSON,你再解析一次是没什么帮助的。</p></li> <li><p><strong>重试操作</strong>。对于那些来自网络和远程服务的错误,有的时候重试操作就可以解决问题。比如,远程服务返回了503(服务不可用错误),你可能会在几秒种后重试。<strong>如果确定要重试,你应该清晰的用文档记录下将会多次重试,重试多少次直到失败,以及两次重试的间隔。</strong> 另外,不要每次都假设需要重试。如果在栈中很深的地方(比如,被一个客户端调用,而那个客户端被另外一个由用户操作的客户端控制),这种情形下快速失败让客户端去重试会更好。如果栈中的每一层都觉得需要重试,用户最终会等待更长的时间,因为每一层都没有意识到下层同时也在尝试。</p></li> <li><p><strong>直接崩溃</strong>。对于那些本不可能发生的错误,或者由程序员失误导致的错误(比如无法连接到同一程序里的本地套接字),可以记录一个错误日志然后直接崩溃。其它的比如内存不足这种错误,是JavaScript这样的脚本语言无法处理的,崩溃是十分合理的。(即便如此,在<code>child_process.exec</code>这样的分离的操作里,得到<code>ENOMEM</code>错误,或者那些你可以合理处理的错误时,你应该考虑这么做)。在你无计可施需要让管理员做修复的时候,你也可以直接崩溃。如果你用光了所有的文件描述符或者没有访问配置文件的权限,这种情况下你���么都做不了,只能等某个用户登录系统把东西修好。</p></li> <li><p><strong>记录错误,其他什么都不做</strong>。有的时候你什么都做不了,没有操作可以重试或者放弃,没有任何理由崩溃掉应用程序。举个例子吧,你用DNS跟踪了一组远程服务,结果有一个DNS失败了。除了记录一条日志并且继续使用剩下的服务以外,你什么都做不了。但是,你至少得记录点什么(凡事都有例外。如果这种情况每秒发生几千次,而你又没法处理,那每次发生都记录可能就不值得了,但是要周期性的记录)。</p></li> </ul> <h3>(没有办法)处理程序员的失误</h3> <p>对于程序员的失误没有什么好做的。从定义上看,一段本该工作的代码坏掉了(比如变量名敲错),你不能用更多的代码再去修复它。一旦你这样做了,你就使用错误处理的代码代替了出错的代码。</p> <p>有些人赞成从程序员的失误中恢复,也就是让当前的操作失败,但是继续处理请求。这种做法不推荐。考虑这样的情况:原始代码里有一个失误是没考虑到某种特殊情况。你怎么确定这个问题不会影响其他请求呢?如果其它的请求共享了某个状态(服务器,套接字,数据库连接池等),有极大的可能其他请求会不正常。</p> <p>典型的例子是REST服务器(比如用Restify搭的),如果有一个请求处理函数抛出了一个<code>ReferenceError</code>(比如,变量名打错)。继续运行下去很有肯能会导致严重的Bug,而且极其难发现。例如:</p> <ol> <li>一些请求间共享的状态可能会被变成<code>null</code>,<code>undefined</code>或者其它无效值,结果就是下一个请求也失败了。</li> <li>数据库(或其它)连接可能会被泄露,降低了能够并行处理的请求数量。最后只剩下几个可用连接会很坏,将导致请求由并行变成串行被处理。</li> <li>更糟的是, postgres 连接会被留在打开的请求事务里。这会导致 postgres “持有”表中某一行的旧值,因为它对这个事务可见。这个问题会存在好几周,造成表无限制的增长,后续的请求全都被拖慢了,从几毫秒到几分钟[脚注4]。虽然这个问题和 postgres 紧密相关,但是它很好的说明了程序员一个简单的失误会让应用程序陷入一种非常可怕的状态。</li> <li>连接会停留在已认证的状态,并且被后续的连接使用。结果就是在请求里搞错了用户。</li> <li>套接字会一直打开着。一般情况下 NodeJS 会在一个空闲的套接字上应用两分钟的超时,但这个值可以覆盖,这将会泄露一个文件描述符。如果这种情况不断发生,程序会因为用光了所有的文件描述符而强退。即使不覆盖这个超时时间,客户端会挂两分钟直到 “hang-up” 错误的发生。这两分钟的延迟会让问题难于处理和调试。</li> <li>很多内存引用会被遗留。这会导致泄露,进而导致内存耗尽,GC需要的时间增加,最后性能急剧下降。这点非常难调试,而且很需要技巧与导致造成泄露的失误联系起来。</li> </ol> <p><strong>最好的从失误恢复的方法是立刻崩溃</strong>。你应该用一个restarter 来启动你的程序,在奔溃的时候自动重启。如果restarter 准备就绪,崩溃是失误来临时最快的恢复可靠服务的方法。</p> <p>奔溃应用程序唯一的负面影响是相连的客户端临时被扰乱,但是记住:</p> <ul> <li>从定义上看,这些错误属于Bug。我们并不是在讨论正常的系统或是网络错误,而是程序里实际存在的Bug。它们应该在线上很罕见,并且是调试和修复的最高优先级。</li> <li>上面讨论的种种情形里,请求没有必要一定得成功完成。请求可能成功完成,可能让服务器再次崩溃,可能以某种明显的方式不正确的完成,或者以一种很难调试的方式错误的结束了。</li> <li>在一个完备的分布式系统里,客户端必须能够通过重连和重试来处理服务端的错误。不管 NodeJS 应用程序是否被允许崩溃,网络和系统的失败已经是一个事实了。</li> <li>如果你的线上代码如此频繁地崩溃让连接断开变成了问题,那么正真的问题是你的服务器Bug太多了,而不是因为你选择出错就崩溃。</li> </ul> <p>如果出现服务器经常崩溃导致客户端频繁掉线的问题,你应该把经历集中在造成服务器崩溃的Bug上,把它们变成可捕获的异常,而不是在代码明显有问题的情况下尽可能地避免崩溃。调试这类问题最好的方法是,把 NodeJS 配置成出现未捕获异常时把内核文件打印出来。在 GNU/Linux 或者 基于 illumos 的系统上使用这些内核文件,你不仅查看应用崩溃时的堆栈记录,还可以看到传递给函数的参数和其它的 JavaScript 对象,甚至是那些在闭包里引用的变量。即使没有配置 code dumps,你也可以用堆栈信息和日志来开始处理问题。</p> <p>最后,记住程序员在服务器端的失误会造成客户端的操作失败,还有客户端必须处理好服务器端的奔溃和网络中断。这不只是理论,而是实际发生在线上环境里。</p> <h2>编写函数的实践</h2> <p>我们已经讨论了如何处理异常,那么当你在编写新的函数的时候,怎么才能向调用者传递错误呢?</p> <p>最最重要的一点是为你的函数写好文档,包括它接受的参数(附上类型和其它约束),返回值,可能发生的错误,以及这些错误意味着什么。 <strong>如果你不知道会导致什么错误或者不了解错误的含义,那你的应用程序正常工作就是一个巧合。</strong> 所以,当你编写新的函数的时候,一定要告诉调用者可能发生哪些错误和错误的含义。</p> <h3>Throw, Callback 还是 EventEmitter</h3> <p>函数有三种基本的传递错误的模式。</p> <ul> <li><p><code>throw</code>以同步的方式传递异常--也就是在函数被调用处的相同的上下文。如果调用者(或者调用者的调用者)用了<code>try/catch</code>,则异常可以捕获。如果所有的调用者都没有用,那么程序通常情况下会崩溃(异常也可能会被<code>domains</code>或者进程级的<code>uncaughtException</code>捕捉到,详见下文)。</p></li> <li><p>Callback 是最基础的异步传递事件的一种方式。用户传进来一个函数(callback),之后当某个异步操作完成后调用这个 callback。通常 callback 会以<code>callback(err,result)</code>的形式被调用,这种情况下, err和 result必然有一个是非空的,取决于操作是成功还是失败。</p></li> <li><p>更复杂的情形是,函数没有用 Callback 而是返回一个 EventEmitter 对象,调用者需要监听这个对象的 error事件。这种方式在两种情况下很有用。</p></li> <li><p>当你在做一个可能会产生多个错误或多个结果的复杂操作的时候。比如,有一个请求一边从数据库取数据一边把数据发送回客户端,而不是等待所有的结果一起到达。在这个例子里,没有用 callback,而是返回了一个 EventEmitter,每个结果会触发一个<code>row</code> 事件,当所有结果发送完毕后会触发<code>end</code>事件,出现错误时会触发一个<code>error</code>事件。</p></li> <li><p>用在那些具有复杂状态机的对象上,这些对象往往伴随着大量的异步事件。例如,一个套接字是一个EventEmitter,它可能会触发“connect“,”end“,”timeout“,”drain“,”close“事件。这样,很自然地可以把”error“作为另外一种可以被触发的事件。在这种情况下,清楚知道”error“还有其它事件何时被触发很重要,同时被触发的还有什么事件(例如”close“),触发的顺序,还有套接字是否在结束的时候处于关闭状态。</p></li> </ul> <p>在大多数情况下,我们会把 callback 和 event emitter 归到同一个“异步错误传递”篮子里。如果你有传递异步错误的需要,你通常只要用其中的一种而不是同时使用。</p> <p>那么,什么时候用<code>throw</code>,什么时候用callback,什么时候又用 EventEmitter 呢?这取决于两件事:</p> <ul> <li>这是操作失败还是程序员的失误?</li> <li>这个函数本身是同步的还是异步的。</li> </ul> <p>直到目前,最常见的例子是在异步函数里发生了操作失败。在大多数情况下,你需要写一个以回调函数作为参数的函数,然后你会把异常传递给这个回调函数。这种方式工作的很好,并且被广泛使用。例子可参照 NodeJS 的<code>fs</code>模块。如果你的场景比上面这个还复杂,那么你可能就得换用 EventEmitter 了,不过你也还是在用异步方式传递这个错误。</p> <p>其次常见的一个例子是像<code>JSON.parse</code>这样的函数同步产生了一个异常。对这些函数而言,如果遇到操作失败(比如无效输入),你得用同步的方式传递它。你可以抛出(更加常见)或者返回它。</p> <p>对于给定的函数,如果有一个异步传递的异常,那么所有的异常都应该被异步传递。可能有这样的情况,请求一到来你就知道它会失败,并且知道不是因为程序员的失误。可能的情形是你缓存了返回给最近请求的错误。虽然你知道请求一定失败,但是你还是应该用异步的方式传递它。</p> <p>通用的准则就是 <strong>你即可以同步传递错误(抛出),也可以异步传递错误(通过传给一个回调函数或者触发EventEmitter的 <code>error</code>事件),但是不用同时使用</strong>。以这种方式,用户处理异常的时候可以选择用回调函数还是用<code>try/catch</code>,但是不需要两种都用。具体用哪一个取决于异常是怎么传递的,这点得在文档里说明清楚。</p> <p>差点忘了程序员的失误。回忆一下,它们其实是Bug。在函数开头通过检查参数的类型(或是其它约束)就可以被立即发现。一个退化的例子是,某人调用了一个异步的函数,但是没有传回调函数。你应该立刻把这个错抛出,因为程序已经出错而在这个点上最好的调试的机会就是得到一个堆栈信息,如果有内核信息就更好了。</p> <p>因为程序员的失误永远不应该被处理,上面提到的调用者只能用<code>try/catch</code>或者回调函数(或者 EventEmitter)其中一种处理异常的准则并没有因为这条意见而改变。如果你想知道更多,请见上面的 (不要)处理程序员的失误。</p> <p>下表以 NodeJS 核心模块的常见函数为例,做了一个总结,大致按照每种问题出现的频率来排列:</p> <table><thead> <tr> <th>函数</th> <th>类型</th> <th>错误</th> <th>错误类型</th> <th>传递方式</th> <th>调用者</th> </tr> </thead><tbody> <tr> <td><code>fs.stat</code></td> <td>异步</td> <td>file not found</td> <td>操作失败</td> <td>callback</td> <td>handle</td> </tr> <tr> <td><code>JSON.parse</code></td> <td>同步</td> <td>bad user input</td> <td>操作失败</td> <td>throw</td> <td><code>try/catch</code></td> </tr> <tr> <td><code>fs.stat</code></td> <td>异步</td> <td>null for filename</td> <td>失误</td> <td>throw</td> <td>none (crash)</td> </tr> </tbody></table> <p>异步函数里出现操作错误的例子(第一行)是最常见的。在同步函数里发生操作失败(第二行)比较少见,除非是验证用户输入。程序员失误(第三行)除非是在开发环境下,否则永远都不应该出现。</p> <p><em>吐槽:程序员失误还是操作失败?</em></p> <p>你怎么知道是程序员的失误还是操作失败呢?很简单,你自己来定义并且记在文档里,包括允许什么类型的函数,怎样打断它的执行。如果你得到的异常不是文档里能接受的,那就是一个程序员失误。如果在文档里写明接受但是暂时处理不了的,那就是一个操作失败。</p> <p>你得用你的判断力去决定你想做到多严格,但是我们会给你一定的意见。具体一些,想象有个函数叫做“connect”,它接受一个IP地址和一个回调函数作为参数,这个回调函数会在成功或者失败的时候被调用。现在假设用户传进来一个明显不是IP地址的参数,比如<code>“bob”</code>,这个时候你有几种选择:</p> <ul> <li>在文档里写清楚只接受有效的IPV4的地址,当用户传进来<code>“bob”</code>的时候抛出一个异常。强烈推荐这种做法。</li> <li>在文档里写上接受任何string类型的参数。如果用户传的是<code>“bob”</code>,触发一个异步错误指明无法连接到<code>“bob”</code>这个IP地址。</li> </ul> <p>这两种方式和我们上面提到的关于操作失败和程序员失误的指导原则是一致的。你决定了这样的输入算是程序员的失误还是操作失败。通常,用户输入的校验是很松的,为了证明这点,可以看<code>Date.parse</code>这个例子,它接受很多类型的输入。但是对于大多数其它函数,我们强烈建议你偏向更严格而不是更松。你的程序越是猜测用户的本意(使用隐式的转换,无论是JavaScript语言本身这么做还是有意为之),就越是容易猜错。本意是想让开发者在使用的时候不用更加具体,结果却耗费了人家好几个小时在Debug上。再说了,如果你觉得这是个好主意,你也可以在未来的版本里让函数不那么严格,但是如果你发现由于猜测用户的意图导致了很多恼人的bug,要修复它的时候想保持兼容性就不大可能了。</p> <p>所以如果一个值怎么都不可能是有效的(本该是string却得到一个<code>undefined</code>,本该是string类型的IP但明显不是),你应该在文档里写明是这不允许的并且立刻抛出一个异常。只要你在文档里写的清清楚楚,那这就是一个程序员的失误而不是操作失败。立即抛出可以把Bug带来的损失降到最小,并且保存了开发者可以用来调试这个问题的信息(例如,调用堆栈,如果用内核文件还可以得到参数和内存分布)。</p> <p>那么 <code>domains</code> 和 <code>process.on(&#39;uncaughtException&#39;)</code> 呢?</p> <p>操作失败总是可以被显示的机制所处理的:捕获一个异常,在回调里处理错误,或者处理EventEmitter的“error”事件等等。<code>Domains</code>以及进程级别的<code>‘uncaughtException’</code>主要是用来从未料到的程序错误恢复的。由于上面我们所讨论的原因,这两种方式都不鼓励。</p> <h2>编写新函数的具体建议</h2> <p>我们已经谈论了很多指导原则,现在让我们具体一些。</p> <ol> <li>你的函数做什么得很清楚。</li> </ol> <p>这点非常重要。每个接口函数的文档都要很清晰的说明: - 预期参数 - 参数的类型 - 参数的额外约束(例如,必须是有效的IP地址)</p> <p>如果其中有一点不正确或者缺少,那就是一个程序员的失误,你应该立刻抛出来。</p> <p>此外,你还要记录:</p> <ul> <li>调用者可能会遇到的操作失败(以及它们的<code>name</code>)</li> <li>怎么处理操作失败(例如是抛出,传给回调函数,还是被 EventEmitter 发出)</li> <li>返回值</li> </ul> <ol> <li>使用 Error 对象或它的子类,并且实现 Error 的协议。</li> </ol> <p>你的所有错误要么使用 Error 类要么使用它的子类。你应该提供<code>name</code>和<code>message</code>属性,<code>stack</code>也是(注意准确)。</p> <ol> <li>在程序里通过 Error 的 <code>name</code> 属性区分不同的错误。</li> </ol> <p>当你想要知道错误是何种类型的时候,用name属性。 JavaScript内置的供你重用的名字包括“RangeError”(参数超出有效范围)和“TypeError”(参数类型错误)。而HTTP异常,通常会用RFC指定的名字,比如“BadRequestError”或者“ServiceUnavailableError”。</p> <p>不要想着给每个东西都取一个新的名字。如果你可以只用一个简单的InvalidArgumentError,就不要分成 InvalidHostnameError,InvalidIpAddressError,InvalidDnsError等等,你要做的是通过增加属性来说明那里出了问题(下面会讲到)。</p> <ol> <li>用详细的属性来增强 Error 对象。</li> </ol> <p>举个例子,如果遇到无效参数,把 <code>propertyName</code> 设成参数的名字,把 <code>propertyValue</code> 设成传进来的值。如果无法连到服务器,用 <code>remoteIp</code> 属性指明尝试连接到的 IP。如果发生一个系统错误,在<code>syscal</code> 属性里设置是哪个系统调用,并把错误代码放到<code>errno</code>属性里。具体你可以查看附录,看有哪些样例属性可以用。</p> <p>至少需要这些属性:</p> <p><code>name</code>:用于在程序里区分众多的错误类型(例如参数非法和连接失败)</p> <p><code>message</code>:一个供人类阅读的错误消息。对可能读到这条消息的人来说这应该已经足够完整。如果你从更底层的地方传递了一个错误,你应该加上一些信息来说明你在做什么。怎么包装异常请往下看。</p> <p><code>stack</code>:一般来讲不要随意扰乱堆栈信息。甚至不要增强它。V8引擎只有在这个属性被读取的时候才会真的去运算,以此大幅提高处理异常时候的性能。如果你读完再去增强它,结果就会多付出代价,哪怕调用者并不需要堆栈信息。</p> <p>你还应该在错误信息里提供足够的消息,这样调用者不用分析你的错误就可以新建自己的错误。它们可能会本地化这个错误信息,也可能想要把大量的错误聚集到一起,再或者用不同的方式显示错误信息(比如在网页上的一个表格里,或者高亮显示用户错误输入的字段)。</p> <ol> <li>若果你传递一个底层的错误给调用者,考虑先包装一下。</li> </ol> <p>经常会发现一个异步函数<code>funcA</code>调用另外一个异步函数<code>funcB</code>,如果<code>funcB</code>抛出了一个错误,希望<code>funcA</code>也抛出一模一样的错误。(请注意,第二部分并不总是跟在第一部分之后。有的时候<code>funcA</code>会重新尝试。有的时候又希望<code>funcA</code>忽略错误因为无事可做。但在这里,我们只讨论<code>funcA</code>直接返回<code>funcB</code>错误的情况)</p> <p>在这个例子里,可以考虑包装这个错误而不是直接返回它。包装的意思是继续抛出一个包含底层信息的新的异常,并且带上当前层的上下文。用 <strong><code>verror</code></strong> 这个包可以很简单的做到这点。</p> <p>举个例子,假设有一个函数叫做 <code>fetchConfig</code>,这个函数会到一个远程的数据库取得服务器的配置。你可能会在服务器启动的时候调用这个函数。整个流程看起来是这样的:</p> <p>1.加载配置 1.1 连接数据库 1.1.1 解析数据库服务器的DNS主机名 1.1.2 建立一个到数据库服务器的TCP连接 1.1.3 向数据库服务器认证 1.2 发送DB请求 1.3 解析返回结果 1.4 加载配置 2 开始处理请求</p> <p>假设在运行时出了一个问题连接不到数据库服务器。如果连接在 1.1.2 的时候因为没有到主机的路由而失败了,每个层都不加处理地都把异常向上抛出给调用者。你可能会看到这样的异常信息:</p> <div class="highlight"><pre><code class="language-sh" data-lang="sh">myserver: Error: connect ECONNREFUSED </code></pre></div> <p>这显然没什么大用。</p> <p>另一方面,如果每一层都把下一层返回的异常包装一下,你可以得到更多的信息:</p> <div class="highlight"><pre><code class="language-sh" data-lang="sh">myserver: failed to start up: failed to load configuration: failed to connect to database server: failed to connect to 127.0.0.1 port 1234: connect ECONNREFUSED。 </code></pre></div> <p>你可能会想跳过其中几层的封装来得到一条不那么充满学究气息的消息:</p> <div class="highlight"><pre><code class="language-sh" data-lang="sh">myserver: failed to load configuration: connection refused from database at 127.0.0.1 port 1234. </code></pre></div> <p>不过话又说回来,报错的时候详细一点总比信息不够要好。</p> <p>如果你决定封装一个异常了,有几件事情要考虑:</p> <ul> <li><p>保持原有的异常完整不变,保证当调用者想要直接用的时候底层的异常还可用。</p></li> <li><p>要么用原有的名字,要么显示地选择一个更有意义的名字。例如,最底层是 NodeJS 报的一个简单的Error,但在步骤1中可以是个 IntializationError 。(但是如果程序可以通过其它的属性区分,不要觉得有责任取一个新的名字)</p></li> <li><p>保留原错误的所有属性。在合适的情况下增强<code>message</code>属性(但是不要在原始的异常上修改)。浅拷贝其它的像是<code>syscall</code>,<code>errno</code>这类的属性。最好是直接拷贝除了 <code>name</code>,<code>message</code>和<code>stack</code>以外的所有属性,而不是硬编码等待拷贝的属性列表。不要理会<code>stack</code>,因为即使是读取它也是相对昂贵的。如果调用者想要一个合并后的堆栈,它应该遍历错误原因并打印每一个错误的堆栈。</p></li> </ul> <p>在Joyent,我们使用 <strong><code>verror</code></strong> 这个模块来封装错误,因为它的语法简洁。写这篇文章的时候,它还不能支持上面的所有功能,但是会被扩���以期支持。</p> <h2>例子</h2> <p>考虑有这样的一个函数,这个函数会异步地连接到一个IPv4地址的TCP端口。我们通过例子来看文档怎么写:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="cm">/*</span> <span class="cm">* Make a TCP connection to the given IPv4 address. Arguments:</span> <span class="cm">*</span> <span class="cm">* ip4addr a string representing a valid IPv4 address</span> <span class="cm">*</span> <span class="cm">* tcpPort a positive integer representing a valid TCP port</span> <span class="cm">*</span> <span class="cm">* timeout a positive integer denoting the number of milliseconds</span> <span class="cm">* to wait for a response from the remote server before</span> <span class="cm">* considering the connection to have failed.</span> <span class="cm">*</span> <span class="cm">* callback invoked when the connection succeeds or fails. Upon</span> <span class="cm">* success, callback is invoked as callback(null, socket),</span> <span class="cm">* where `socket` is a Node net.Socket object. Upon failure,</span> <span class="cm">* callback is invoked as callback(err) instead.</span> <span class="cm">*</span> <span class="cm">* This function may fail for several reasons:</span> <span class="cm">*</span> <span class="cm">* SystemError For &quot;connection refused&quot; and &quot;host unreachable&quot; and other</span> <span class="cm">* errors returned by the connect(2) system call. For these</span> <span class="cm">* errors, err.errno will be set to the actual errno symbolic</span> <span class="cm">* name.</span> <span class="cm">*</span> <span class="cm">* TimeoutError Emitted if &quot;timeout&quot; milliseconds elapse without</span> <span class="cm">* successfully completing the connection.</span> <span class="cm">*</span> <span class="cm">* All errors will have the conventional &quot;remoteIp&quot; and &quot;remotePort&quot; properties.</span> <span class="cm">* After any error, any socket that was created will be closed.</span> <span class="cm">*/</span> <span class="kd">function</span> <span class="nx">connect</span><span class="p">(</span><span class="nx">ip4addr</span><span class="p">,</span> <span class="nx">tcpPort</span><span class="p">,</span> <span class="nx">timeout</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">typeof</span> <span class="p">(</span><span class="nx">ip4addr</span><span class="p">),</span> <span class="s1">&#39;string&#39;</span><span class="p">,</span> <span class="s2">&quot;argument &#39;ip4addr&#39; must be a string&quot;</span><span class="p">);</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">ok</span><span class="p">(</span><span class="nx">net</span><span class="p">.</span><span class="nx">isIPv4</span><span class="p">(</span><span class="nx">ip4addr</span><span class="p">),</span> <span class="s2">&quot;argument &#39;ip4addr&#39; must be a valid IPv4 address&quot;</span><span class="p">);</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">typeof</span> <span class="p">(</span><span class="nx">tcpPort</span><span class="p">),</span> <span class="s1">&#39;number&#39;</span><span class="p">,</span> <span class="s2">&quot;argument &#39;tcpPort&#39; must be a number&quot;</span><span class="p">);</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">ok</span><span class="p">(</span><span class="o">!</span><span class="nb">isNaN</span><span class="p">(</span><span class="nx">tcpPort</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">tcpPort</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">tcpPort</span> <span class="o">&lt;</span> <span class="mi">65536</span><span class="p">,</span> <span class="s2">&quot;argument &#39;tcpPort&#39; must be a positive integer between 1 and 65535&quot;</span><span class="p">);</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">typeof</span> <span class="p">(</span><span class="nx">timeout</span><span class="p">),</span> <span class="s1">&#39;number&#39;</span><span class="p">,</span> <span class="s2">&quot;argument &#39;timeout&#39; must be a number&quot;</span><span class="p">);</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">ok</span><span class="p">(</span><span class="o">!</span><span class="nb">isNaN</span><span class="p">(</span><span class="nx">timeout</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">timeout</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">&quot;argument &#39;timeout&#39; must be a positive integer&quot;</span><span class="p">);</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">typeof</span> <span class="p">(</span><span class="nx">callback</span><span class="p">),</span> <span class="s1">&#39;function&#39;</span><span class="p">);</span> <span class="cm">/* do work */</span> <span class="p">}</span> </code></pre></div> <p>这个例子在概念上很简单,但是展示了上面我们所谈论的一些建议:</p> <ul> <li><p>参数,类型以及其它一些约束被清晰的文档化。</p></li> <li><p>这个函数对于接受的参数是非常严格的,并且会在得到错误参数的时候抛出异常(程序员的失误)。</p></li> <li><p>可能出现的操作失败集合被记录了。通过不同的”name“值可以区分不同的异常,而”errno“被用来获得系统错误的详细信息。</p></li> <li><p>异常被传递的方式也被记录了(通过失败时调用回调函数)。</p></li> <li><p>返回的错误有”remoteIp“和”remotePort“字段,这样用户就可以定义自己的错误了(比如,一个HTTP客户端的端口号是隐含的)。</p></li> <li><p>虽然很明显,但是连接失败后的状态也被清晰的记录了:所有被打开的套接字此时已经被关闭。</p></li> </ul> <p>这看起来像是给一个很容易理解的函数写了超过大部分人会写的的超长注释,但大部分函数实际上没有这么容易理解。所有建议都应该被有选择的吸收,如果事情很简单,你应该自己做出判断,但是记住:用十分钟把预计发生的记录下来可能之后会为你或其他人节省数个小时。</p> <h2>总结</h2> <ul> <li><p>学习了怎么区分操作失败,即那些可以被预测的哪怕在正确的程序里也无法避免的错误(例如,无法连接到服务器);而程序的Bug则是程序员失误。</p></li> <li><p>操作失败可以被处理,也应当被处理。程序员的失误无法被处理或可靠地恢复(本不应该这么做),尝试这么做只会让问题更难调试。</p></li> <li><p>一个给定的函数,它处理异常的方式要么是同步(用throw方式)要么是异步的(用callback或者EventEmitter),不会两者兼具。用户可以在回调函数里处理错误,也可以使用 <code>try/catch</code>捕获异常 ,但是不能一起用。实际上,使用throw并且期望调用者使用 <code>try/catch</code> 是很罕见的,因为 NodeJS 里的同步函数通常不会产生运行失败(主要的例外是类似于<code>JSON.parse</code>的用户输入验证函数)。</p></li> <li><p>在写新函数的时候,用文档清楚地记录函数预期的参数,包括它们的类型、是否有其它约束(例如必须是有效的IP地址),可能会发生的合理的操作失败(例如无法解析主机名,连接服务器失败,所有的服务器端错误),错误是怎么传递给调用者的(同步,用<code>throw</code>,还是异步,用 callback 和 EventEmitter)。</p></li> <li><p>缺少参数或者参数无效是程序员的失误,一旦发生总是应该抛出异常。函数的作者认为的可接受的参数可能会有一个灰色地带,但是如果传递的是一个文档里写明接收的参数以外的东西,那就是一个程序员失误。</p></li> <li><p>传递错误的时候用标准的 Error 类和它标准的属性。尽可能把额外的有用信息放在对应的属性里。如果有可能,用约定的属性名(如下)。</p></li> </ul> <h1>附录:Error 对象属性命名约定</h1> <p>强烈建议你在发生错误的时候用这些名字来保持和Node核心以及Node插件的一致。这些大部分不会和某个给定的异常对应,但是出现疑问的时候,你应该包含任何看起来有用的信息,即从编程上也从自定义的错误消息上。【表】。</p> <table><thead> <tr> <th style="text-align: right">Property name</th> <th style="text-align: left">Intended use</th> </tr> </thead><tbody> <tr> <td style="text-align: right">localHostname</td> <td style="text-align: left">the local DNS hostname (e.g., that you&#39;re accepting connections at)</td> </tr> <tr> <td style="text-align: right">localIp</td> <td style="text-align: left">the local IP address (e.g., that you&#39;re accepting connections at)</td> </tr> <tr> <td style="text-align: right">localPort</td> <td style="text-align: left">the local TCP port (e.g., that you&#39;re accepting connections at)</td> </tr> <tr> <td style="text-align: right">remoteHostname</td> <td style="text-align: left">the DNS hostname of some other service (e.g., that you tried to connect to)</td> </tr> <tr> <td style="text-align: right">remoteIp</td> <td style="text-align: left">the IP address of some other service (e.g., that you tried to connect to)</td> </tr> <tr> <td style="text-align: right">remotePort</td> <td style="text-align: left">the port of some other service (e.g., that you tried to connect to)</td> </tr> <tr> <td style="text-align: right">path</td> <td style="text-align: left">the name of a file, directory, or Unix Domain Socket (e.g., that you tried to open)</td> </tr> <tr> <td style="text-align: right">srcpath</td> <td style="text-align: left">the name of a path used as a source (e.g., for a rename or copy)</td> </tr> <tr> <td style="text-align: right">dstpath</td> <td style="text-align: left">the name of a path used as a destination (e.g., for a rename or copy)</td> </tr> <tr> <td style="text-align: right">hostname</td> <td style="text-align: left">a DNS hostname (e.g., that you tried to resolve)</td> </tr> <tr> <td style="text-align: right">ip</td> <td style="text-align: left">an IP address (e.g., that you tried to reverse-resolve)</td> </tr> <tr> <td style="text-align: right">propertyName</td> <td style="text-align: left">an object property name, or an argument name (e.g., for a validation error)</td> </tr> <tr> <td style="text-align: right">propertyValue</td> <td style="text-align: left">an object property value (e.g., for a validation error)</td> </tr> <tr> <td style="text-align: right">syscall</td> <td style="text-align: left">the name of a system call that failed</td> </tr> <tr> <td style="text-align: right">errno</td> <td style="text-align: left">the symbolic value of errno (e.g., &quot;ENOENT&quot;). Do not use this for errors that don&#39;t actually set the C value of errno.Use &quot;name&quot; to distinguish between types of errors.</td> </tr> </tbody></table> <h2>脚注</h2> <ol> <li><p>人们有的时候会这么写代码,他们想要在出现异步错误的时候调用 callback 并把错误作为参数传递。他们错误地认为在自己的回调函数(传递给 <code>doSomeAsynchronousOperation</code> 的函数)里<code>throw</code> 一个异常,会被外面的catch代码块捕获。<code>try/catch</code>和异步函数不是这么工作的。回忆一下,异步函数的意义就在于被调用的时候<code>myApiFunc</code>函数已经返回了。这意味着try代码块已经退出了。这个回调函数是由Node直接调用的,外面并没有try的代码块。如果你用这个反模式,结果就是抛出异常的时候,程序崩溃了。</p></li> <li><p>在JavaScript里,抛出一个不属于Error的参数从技术上是可行的,但是应该被避免。这样的结果使获得调用堆栈没有可能,代码也无法检查”name“属性,或者其它任何能够说明哪里有问题的属性。</p></li> <li><p>操作失败和程序员的失误这一概念早在NodeJS之前就已经存在存在了。不严格地对应者Java里的checked和unchecked异常,虽然操作失败被认为是无法避免的,比如 OutOfMemeoryError,被归为uncheked异常。在C语言里有对应的概念,普通异常处理和使用断言。维基百科上关于断言的的文章也有关于什么时候用断言什么时候用普通的错误处理的类似的解释。</p></li> <li><p>如果这看起来非常具体,那是因为我们在产品环境中遇到这样过这样的问题。这真的很可怕。</p></li> </ol> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师编译并整理 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> 为何响应时间常被测错 /pm/2015/04/09/responsetime/ Thu, 09 Apr 2015 00:00:00 +0000 /pm/2015/04/09/responsetime <h2>为何响应时间常被测错</h2> <p>响应时间在许多情况下都是性能分析的基础。它们处于预期的界限内时,一切正常;而一旦过高,我们就得开始优化应用。</p> <p>因此响应时间在性能监测和分析中扮演着核心角色。在虚拟化和云环境中,它们也是您能得到的最准确的性能指标。但很多情况下,人们却以错误的方式测量并解释响应时间。为此我们有充足的理由来讨论响应时间测量以及如何对其进行解释这一话题。下面我将讨论典型测量手段、相关的误解以及如何改善测量手段等问题。</p> <h3>信息被平均丢了</h3> <p>在测量响应时间时,我们无法逐个查看测量结果。即便是在非常小的生产系统中,事务数量也多到难以应付。因此测量结果都要在某一时间框架内汇总到一起。按照应用配置的不同,这可能是几秒、几分甚至几小时。</p> <p>尽管这种汇总帮助我们轻松理解大型系统中的响应时间,但也意味着我们丢失了信息。最常见的测量汇总方法是采用平均值。这就是说所采集到的测量结果被平均到一起,这样我们处理的就是平均值,而非真实值。 平均值的问题在于它们在很多情况下并没有反映真实世界中发生的情况。使用平均值时,之所以会引起错误或者误导性结果,主要原因共有两个。</p> <p>如果测量结果取值波动较大,那么平均值无法代表实际测量的响应时间。如测量结果的范围介于1 到 4 秒之间,那么平均值可能是在 2 秒左右,这当然并不代表很多用户的感受。 因此平均值仅对真实世界的性能提供了很少的帮助。所以不应该只使用平均值,而应使用百分位数。如果您与那些在性能领域工作过一段时间的人们交流,他们会告诉你用起来可靠的唯一指标就是百分位数。与平均值相反,百分位数定义了有多少用户体会到的响应时间低于某一门限值。举例来说,如果第 50 百分位数为 2.5 秒,则意味着对于百分之 50 的用户,响应时间都小于或者等于 2.5 秒。可以看出,这种手段与平均值相比更接近于现实。</p> <p><img src="/images/responsetime.png" alt="ruby-prof"></p> <p><strong>测量结果序列的百分位数和平均值</strong></p> <p>百分位数唯一的潜在缺陷是它们要求的数据存储量要高于平均值。平均值计算仅要求所有测量结果的总和以及数量,而百分位数的计算却更为复杂,它要求整个范围内的测量结果取值。正是由于这个原因,并不是所有性能管理工具都支持百分位数。</p> <h3>所有东西都被混到一起</h3> <p>数据汇总中另一个重要问题是就是采用哪些数据作为汇总依据。如果把不同事务类型的数据混合到一起,比如起始页、查找和信用卡确认,那么结果就鲜有价值,原因就在于基础数据的差别就像苹果和桔子一样。因此除了确保使用百分位数外,还有必要正确拆分事务类型,以使计算所依据的数据能够匹配到一起。</p> <p>按照业务功能来拆分事务的概念通常被称为自定义事务。虽然自定义事务基本理念就是通过一些请求参数来区分应用中的事务,这些参数例如这些事务是干什么的或者来自哪里,或者按照逻辑关系让用户有选择的对事务进行聚合等。比如说“放入购物篮”事务或者某一用户的请求等。</p> <h5>只有综合两种手段才能确保所测响应时间构成性能分析的可靠基础。</h5> Ruby中的Profiling工具 /ruby/2015/04/08/ruby-profilers/ Wed, 08 Apr 2015 00:00:00 +0000 /ruby/2015/04/08/ruby-profilers <h2>Ruby内置的profiler</h2> <p>内置的profiler实现的很简单,在ruby2.2中只有150行代码,大家可以看看它的实现<a href="https://github.com/ruby/ruby/blob/trunk/lib/profiler.rb">profile.rb</a> 。内置的profiler使用起来非常的方便,只需要加上<code>-rprofile</code>参数即可。例如:</p> <p>执行:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby -rprofile test.rb </code></pre></div> <p>输出结果为:</p> <div class="highlight"><pre><code class="language-text" data-lang="text"> % cumulative self self total time seconds seconds calls ms/call ms/call name 24.24 0.16 0.16 10001 0.02 0.06 Object#m2 18.18 0.28 0.12 2 60.00 330.00 Integer#times 18.18 0.40 0.12 10001 0.01 0.06 Object#m1 15.15 0.50 0.10 10001 0.01 0.01 Class#new 10.61 0.57 0.07 10000 0.01 0.01 P1.new 6.06 0.61 0.04 20000 0.00 0.00 Fixnum#to_s 4.55 0.64 0.03 10000 0.00 0.00 Struct#initialize 3.03 0.66 0.02 10000 0.00 0.00 P2#initialize 0.00 0.66 0.00 1 0.00 0.00 TracePoint#enable 0.00 0.66 0.00 1 0.00 0.00 Class#initialize 0.00 0.66 0.00 1 0.00 0.00 nil# 0.00 0.66 0.00 1 0.00 0.00 Struct.new 0.00 0.66 0.00 7 0.00 0.00 Module#method_added 0.00 0.66 0.00 3 0.00 0.00 BasicObject#singleton_method_added 0.00 0.66 0.00 2 0.00 0.00 Class#inherited 0.00 0.66 0.00 2 0.00 0.00 IO#set_encoding 0.00 0.66 0.00 1 0.00 0.00 TracePoint#disable 0.00 0.66 0.00 1 0.00 660.00 #toplevel </code></pre></div> <p>通过打印出的结果能够很明显的看出耗时的方法。内置的profiler很简单,只能打印出这样的结果,没有 其他输出格式的选项,下面介绍的其他几种都有丰富的格式输出。</p> <h2>ruby-prof</h2> <p>repo: <a href="https://github.com/ruby-prof/ruby-prof">https://github.com/ruby-prof/ruby-prof</a></p> <p><code>ruby-prof</code>具有C扩展,所以它能运行的更快,同时它支持丰富的输出格式,方便我们去查找性能瓶颈。 <code>ruby-prof</code>支持输出<code>GraphViz</code>支持的dot格式,两者的安装方法如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">gem install ruby-prof ubuntu | sudo apt-get install graphviz Mac | brew install graphviz </code></pre></div> <p>执行命令很简单,如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">ruby-prof --mode=wall --printer=dot --file=output.dot test.rb 25 </code></pre></div> <p>此命令的详细使用方法请参考帮助信息<code>ruby-prof --help</code>。</p> <p>上面命令的执行结果会输出一个graphviz的dot文件,graphviz提供一个格式转换命令,可以将此文件 转换为一个pdf文件以方便查看,如下:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">dot -T pdf -o output.pdf output.dot </code></pre></div> <p>这样就可以打开output.pdf查看程序内的方法调用占比了。</p> <p><img src="/images/ruby-prof-result.png" alt="ruby-prof"></p> <h2>perftools.rb</h2> <p>repo: <a href="https://github.com/tmm1/perftools.rb">https://github.com/tmm1/perftools.rb</a></p> <p><code>perftools.rb</code>是<code>google-perftools</code>的ruby版本,不过它只支持ruby2.1以下版本,2.1及以上 版本就需要用到下面的<code>stackprof</code>了,这两个工具都是一个人写的。鉴于此,我们略过<code>perftools.rb</code>, 作者实现<code>stackprof</code>,就是为了替代<code>perftools.rb</code>。如果有需求的话,就请参考其github主页。</p> <h2>stackprof</h2> <p>repo: <a href="https://github.com/tmm1/stackprof">https://github.com/tmm1/stackprof</a></p> <p><code>stackprof</code>只支持Ruby2.1+,不过现在ruby的版本发布很快,每一个版本都能带来一些新东西,2.1 应该很快就能成为很基础的版本,我们就在这个版本上来做一些测试。</p> <p>安装:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">gem install stackprof </code></pre></div> <p>这次我们直接在代码中使用stackprof的方法:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">&#39;stackprof&#39;</span> <span class="k">def</span> <span class="nf">m1</span> <span class="mi">5_000_000</span><span class="o">.</span><span class="n">times</span><span class="p">{</span> <span class="mi">1</span><span class="o">+</span><span class="mi">2</span><span class="o">+</span><span class="mi">3</span><span class="o">+</span><span class="mi">4</span><span class="o">+</span><span class="mi">5</span> <span class="p">}</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">m2</span> <span class="mi">1_000_000</span><span class="o">.</span><span class="n">times</span><span class="p">{</span> <span class="mi">1</span><span class="o">+</span><span class="mi">2</span><span class="o">+</span><span class="mi">3</span><span class="o">+</span><span class="mi">4</span><span class="o">+</span><span class="mi">5</span> <span class="p">}</span> <span class="k">end</span> <span class="no">StackProf</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="ss">mode</span><span class="p">:</span> <span class="ss">:cpu</span><span class="p">,</span> <span class="ss">out</span><span class="p">:</span> <span class="s1">&#39;tmp/stackprof.dump&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="n">m1</span> <span class="n">m2</span> <span class="k">end</span> </code></pre></div> <p>我们执行这个ruby程序,<code>ruby test.rb</code>,会在当前目录的tmp目录中产生一个文件<code>stackprof.dump</code>, 然后来分析以下这个文件,<code>stackprof</code>命令本身可以解析这个文件,执行下面的命令:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">stackprof tmp/stackprof.dump --text </code></pre></div> <p>则会产生下面的结果,结果应该是很清晰的,很明显在代码中<code>m1</code>方法要占有绝大部分的运行时间。</p> <div class="highlight"><pre><code class="language-text" data-lang="text">================================== Mode: cpu(1000) Samples: 75 (0.00% miss rate) GC: 0 (0.00%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 62 (82.7%) 62 (82.7%) block in Object#m1 13 (17.3%) 13 (17.3%) block in Object#m2 75 (100.0%) 0 (0.0%) &lt;main&gt; 75 (100.0%) 0 (0.0%) block in &lt;main&gt; 75 (100.0%) 0 (0.0%) &lt;main&gt; 62 (82.7%) 0 (0.0%) Object#m1 13 (17.3%) 0 (0.0%) Object#m2 </code></pre></div> <p>其他更加丰富的输出方式和分析方式,就请参考<code>stackprof</code>的github主页,讲解的很全面。</p> <p>如果你希望在web前端中展示相关信息,就请看看<code>stackprof-webnav</code>这个gem,它提供了比较全面的 展示,操作等等,适合在一些web应用中使用,github地址:<a href="https://github.com/alisnic/stackprof-webnav">stackprof-webnav</a></p> <h2>rack-mini-profiler</h2> <p>repo: <a href="https://github.com/MiniProfiler/rack-mini-profiler">https://github.com/MiniProfiler/rack-mini-profiler</a></p> <p><code>rack-mini-profiler</code>专门应用于基于<code>rack</code>的web应用的性能调优,在rails中的使用方法如下:</p> <p>首先将gem添加到gemfile中:</p> <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">gem</span> <span class="s1">&#39;rack-mini-profiler&#39;</span> </code></pre></div> <p>执行:</p> <div class="highlight"><pre><code class="language-text" data-lang="text">bundle install </code></pre></div> <p>然后重启你的服务器,访问任意的URl,在页面上的左上角会看到响应时间的毫秒数。如下图所示:</p> <p><img src="/images/rack-mini-profiler.png" alt="rack-mini-profiler"></p> <p>点击<code>query time(ms)</code>列中的<code>1 sql</code>则可以查看到执行的sql语句及耗时:</p> <p><img src="/images/rack-mini-profiler-sql.png" alt="rack-mini-profiler-sql"></p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p> NodeJS异常处理uncaughtException篇 /nodejs/2015/04/08/nodejs-uncaughtexception/ Wed, 08 Apr 2015 00:00:00 +0000 /nodejs/2015/04/08/nodejs-uncaughtexception <p>很多 NodeJS 的开发者在抱怨异常处理太麻烦,我们会通过一些列博客梳理一下NodeJS中常见的异常处理的手段。</p> <p>和大多数编程语言一样,在 NodeJS 里可以通过<code>throw</code>抛出一个异常:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Catch me&#39;</span><span class="p">);</span> </code></pre></div> <p>为了捕获这个异常需要把代码包在<code>Try Catch</code>中:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="k">try</span><span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Catch me&#39;</span><span class="p">);</span> <span class="p">}</span><span class="k">catch</span><span class="p">(</span><span class="nx">e</span><span class="p">){</span> <span class="c1">// error captured</span> <span class="p">}</span> </code></pre></div> <p>然而,由于 NodeJS 的异步特性,上述代码只需稍加改造就会失效:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="k">try</span><span class="p">{</span> <span class="nx">process</span><span class="p">.</span><span class="nx">nextTick</span><span class="p">(</span><span class="kd">function</span> <span class="nx">my_app</span><span class="p">(){</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Catch me&#39;</span><span class="p">);</span> <span class="p">})</span> <span class="p">}</span><span class="k">catch</span><span class="p">(</span><span class="nx">e</span><span class="p">){</span> <span class="c1">// never called</span> <span class="p">}</span> </code></pre></div> <p>在 现实世界里,异常总是会产生在某个模块中。所谓模块就是能完成一个功能的单元,即使是一个简单的函数也可以被看做一个模块。随着项目代码行数增多,异步嵌 套的复杂性加强,经常会有异常没捕获的情况发生。一个没有很强健壮性的 NodeJS 应用,会因为一个未捕获的异常就整个挂掉,导致服务不可用。要改变大家觉得 NodeJS 是脆弱的这个认识,需要开发者加深对这门语言异常处理机制的了解。</p> <h2>uncaughtException</h2> <p><code>uncaughtException</code> 其实是 NodeJS 进程的一个事件。如果进程里产生了一个异常而没有被任何<code>Try Catch</code>捕获会触发这个事件。为了简化问题,我们还是先看看同步情况下的例子。</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">function</span> <span class="nx">external</span><span class="p">()</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Catch me&#39;</span><span class="p">);</span> <span class="p">}</span> <span class="kd">function</span> <span class="nx">internal</span><span class="p">()</span> <span class="p">{</span> <span class="nx">external</span><span class="p">();</span> <span class="p">}</span> <span class="nx">internal</span><span class="p">();</span> <span class="c1">//error will be thrown</span> </code></pre></div> <p>在命令行里执行这个程序,脚本会在抛出异常的那一行中断。接下来,由于没有<code>Try Catch</code>,异常会一直冒泡直到事件循环为止,而NodeJS对异常的默认处理非常简单,处理的代码 <em>类似</em> 于:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">function</span> <span class="nx">_MyFatalException</span><span class="p">(</span><span class="nx">err</span><span class="p">){</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="s1">&#39;uncaughtException&#39;</span><span class="p">,</span><span class="nx">err</span><span class="p">)){</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">stack</span><span class="p">);</span> <span class="nx">process</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="s1">&#39;exit&#39;</span><span class="p">,</span><span class="mi">1</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div> <p>NodeJS 对于未捕获异常的默认处理是: - 触发 uncaughtException 事件 - 如果 uncaughtException 没有被监听,那么 - 打印异常的堆栈信息 - 触发进程的 exit 事件</p> <p>如果你正在用 NodeJS 开发服务器,那么你肯定不希望偶然的一个异常让整个服务器挂掉。那么是不是只要监听了 <code>uncaughtException</code> 就可以阻止服务器的进程退出呢? 答案是可以,<strong>但是不要这么做!</strong>。看这个例子:</p> <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;express&#39;</span><span class="p">);</span> <span class="kd">function</span> <span class="nx">external</span><span class="p">(</span><span class="nx">cb</span><span class="p">)</span> <span class="p">{</span> <span class="nx">process</span><span class="p">.</span><span class="nx">nextTick</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">();</span> <span class="nx">cb</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="s1">&#39;sunny&#39;</span><span class="p">);</span> <span class="p">})</span> <span class="p">}</span> <span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span> <span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/weather&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span> <span class="nx">external</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span> <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="s1">&#39;Weather of Beijing is &#39;</span> <span class="o">+</span> <span class="nx">data</span><span class="p">);</span> <span class="p">})</span> <span class="p">})</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">8018</span><span class="p">);</span> <span class="kd">function</span> <span class="nx">noop</span><span class="p">(){}</span> <span class="nx">process</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;uncaughtException&#39;</span><span class="p">,</span> <span class="nx">noop</span><span class="p">)</span> </code></pre></div> <p>上面这个例子假设用户访问站点的时候可以看到当地的天气,我们用 <code>apache2-utils</code> 来模拟请求</p> <div class="highlight"><pre><code class="language-sh" data-lang="sh">ab -n <span class="m">1000</span> -c <span class="m">20</span> http://localhost:8018/weather </code></pre></div> <p>糟糕!请求一直在等待,内存上涨。原因在于<code>res.end</code> 永远不会执行,现有的<code>I/O</code>处于等待的状态,已经开辟的资源不仅不会被释放,而且服务器还在不知疲倦地接受新的用户请求。</p> <p>在 NodeJS 中处理异常是代价高昂的,而且一不小心就会导致内存泄露和让应用程序处于不稳定的状态。为了提高健壮性,我们可以用<code>Cluster模式</code>,由之而来的推荐做法是: - 针对发生异常的请求返回一个错误代码 - 出错的Worker不再接受新的请求 - 退出关闭Worker进程</p> <p><strong>本文由<a href="http://www.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">OneAPM</a>工程师原创文章 ,想阅读更多技术文章,请访问OneAPM<a href="http://code.oneapm.com/?hmsr=media&amp;hmmd=&amp;hmpl=&amp;hmkw=&amp;hmci=">官方技术博客</a>。</strong></p>