Code Monkey home page Code Monkey logo

blogwithmarkdown's People

Contributors

cloudprogram avatar spacewander avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blogwithmarkdown's Issues

Principles of Reactive Programming

三月底的时候,要去实习的公司就已经早早地确定下来了。这么一来,我有了一大段时间可以自由支配,可以学点有趣的东西,在毕业之前多多拓展自己的视野。

抱着这样的念头,外加一时心血来潮,我决定去跟一门MOOC课,看看传说中的MOOC课是什么样子。一阵东挑西选之后,我选中了这么课:Coursera上的《Principles of Reactive Programming》。当时之所以做出这样的选择,主要原因有三:

  1. 当时正好对并发程序开发感兴趣
  2. 这门课开课时间正好对得上
  3. 计算机类的课程,大部分不是简单的算法入门或者编程语言介绍,就是机器学习或数据挖掘之类偏学术的课程。适合我口味的课并不多。

当然啦,这么课还有一点加分项:开课的三位讲师都是大牛,包括scala的作者、ReactiveX的开发者以及Akka的scala版本的开发者。总之,让他们三个讲Reactive编程简直再适合不过了。

既然讲师都是scala背景的,这门课自然是以scala讲述。于是乎,我还得去学scala才能跟上老师的授课。由于报名的时候,离开课只剩一个星期,想学会scala是不可能了,所以在上课的同时,也需要继续完善scala的学习。

这门课给我带来的第一份收获是对scala这门语言的认识。

虽然以后我恐怕不会用scala来开发自己的项目(主要是因为编译耗时太长,拖慢了开发效率),但是scala这门语言也是相当有趣的。它就像是c++一样,同时包含了多项编程范式。你可以把函数式风格和面向对象风格杂糅在一起。不过我最为看中的是,scala就像其他现代静态编程语言一样,把类型推导和匿名函数双剑合璧,大大拓展了语言的表达能力。除了之外,scala还有许多语法糖,比如mixin、操作符重载和时间字面量,具体可以看看 Ruby VS Scala。总之,scala是一门非常华丽的编程语言,具有许多让编程语言控兴奋不已的特性。当然,这么多特性的后果,是复杂到有时语法错误连IDE都发现不了,以及吊打c++的编译耗时。

Ok,该回到主题了,继续聊聊这门课。

课程一开始是介绍函数式编程的一些概念,比如map和flatmap等等。接着开始引入一些具有Reactive风格的概念,比如Signal。然后逐渐进入主题,开始出现两对基友:

One Many
Synchronous T/Try[T] Iterable[T]
Asynchronous Future[T] Observable[T]

把同步的类型T/Try[T]包装起来,就是异步的Future[T]。持有一个Future,等同于持有一个结果(或者包含了该结果的Try类型)。
把同步的类型Iterable[T]包装起来,就是异步的Observable[T]。持有一个Observable[T],就能遍历对应的Iterable[T],不管迭代出来的值是刚刚新鲜出炉的,还是返回原有对象的一部分。

于是我们可以这样设计异步的API,把返回的单个值/可迭代的容器依对应的Future/Observable包装起来,交给用户去决定取值的时间。这样,发生阻塞的决定权由调用函数交给了调用方(因为把执行交给了后台的线程,甚至可以同时执行多个函数,充分发挥并行化的威力)。

按Reactive字面上的意思,就是依据外界的刺激进行响应。所以获取到了外界的刺激,下面就应该作出对应的响应了。

第一部分学到的函数式编程概念在这里就用得上了。我们可以把函数和外界的刺激绑定在一起,当刺激传递过来时,依据刺激的类型(成功还是失败)或者值,调用对应的函数进行处理。scala的模式匹配在这里派上了大用场:

  val pageSubscription: Subscription = pages.observeOn(eventScheduler) subscribe {
      x => {
        x match {
          case Failure(e) => editorpane.text = e.getMessage
          case success: Success[String] => editorpane.text = success.get
        }
     }
  }

外加scala自带和Reactive框架提供的onErrormapflattenmerge等api,基本上对数据的处理就像流一样,你可以随心所欲地发布一条新的支流,也可以合并现有的支流。总之,把刺激逐个处理的想法已经过时了。现在你需要把它们放入流水线上,或者更准确地说,放到神经系统中,让它们按照预先设定的情况去走对应的分支。比起之前的做法,新的做法更加清晰明了。

最后的几周是关于Akka框架的。Akka是一个基于Actor的并发系统。基于Actor的系统基本上都差不多,基本包括下面几条规则:

  1. Actor分为几种不同的状态。
  2. 每个Actor都有一个地址,可以给别的Actor发信息
  3. 信息也分为几种。你可以在信息中夹带数据
  4. 根据收到的信息的不同,Actor可以进行对应的处理,也许会进入新的的状态。

这也是一种Reactive Programming,跟前面的那种不一样的是,这次我们把数据变成了信息,把处理函数变成了状态。感觉这种形式要比前面那种要好写。

于是乎7周的课程就这么有惊无险地过去了。期间有赶deadline的紧张,也有对作业无从下手的焦虑;有顿悟新想法的快乐,也有排除万难拿到10/10的激动。最后终于得到了一个优秀的成绩,给这段MOOC之旅划上美满的句号。

最后的最后,顺便晒一下结课的证书:
https://www.coursera.org/maestro/api/certificate/get_certificate?course_id=974748

跟我一起写shell补全脚本(开篇)

如果你是一个重度shell用户,一定会关注所用的shell的补全功能。某款shell的补全强弱,也许就是决定你的偏好的第一要素。

shell里面补全的影子无处不在,输入命令的时候可以有补全,敲打选项的时候可以有补全,选择文件的时候可以有补全。有些shell甚至支持通过补全来切换版本控制的分支。由于shell里面可以运行的程序千差万别,shell一般不会内置针特定对某个工具的补全功能。与之相对的,shell提供了一些补全用的API,交由用户编写对应的补全脚本。

在这里,我想向大家介绍如何利用提供的API,来编写一个shell补全脚本。由于需要覆盖的内容较多,所以分为Bash和Zsh两篇。也许有fish用户会抱怨,fish又一次被忽略了:D。之所以只有Bash和Zsh的内容,是因为:1. 这两种shell的用户占了shell用户的绝大多数。2. 我没有用过fish,所以对这方面也不了解。希望有人能够锦上添花,写一个fish版本的补全脚本教程。

既然想要写一个shell补全脚本,那么接下来要决定待补全的对象了。这里我选择pandoc作为目标。pandoc是文档转换器中的瑞士军刀,支持主流的各种标记语言,甚至对于PDF和MS Word也有一定程度上的支持。pandoc支持的选项琳琅满目,如果都要实现确实很花时间。所以这里就只实现General options,Reader options,General writer options大部分的内容。不管怎么说,这将会是一个“既不至于简单到让人丧失兴趣,又不至于困难到让人丧失信心”的任务。

安装pandoc的方式见官网上的说明,这里就不赘述了。安装完了之后,man pandoc就能看到各个选项的说明。大体上我们需要实现以下几个目标:

  1. 支持主选项(General options)
  2. 支持子选项(Reader options/General writer options)
  3. 支持给选项提供参数值来源。比如在敲pandoc -f之后,能够补全FORMAT的内容。

好,让我们开始给pandoc写补全脚本吧!

下一篇:跟我一起写shell补全脚本(Bash篇)

Fast Ruby to Julia bindings through the LLVM

Name Zexuan Luo
Github username spacewander
Email [email protected]
Timezone Guangzhou, China, UTC +8
School South China University of Technology(SCUT)
Proposal Title Fast Ruby to Julia bindings through the LLVM

Motivation for Proposal / Goal:

Goal

Julia is a language designed to address the requirements of high-performance numerical and scientific computing. It includes efficient libraries for floating point, linear algebra, random number generation, fast Fourier transforms. If we can call Julia functions directly from Ruby, this can be a great gift to Ruby. The Ruby community will be benefited a lot in science area.

Implementation Details

First, start our journey from C world

Julia is designed to be embedded in C easily. You can call Julia code in C code, like this example:

#include <julia.h>

int main(int argc, char *argv[])
{
    /* required: setup the julia context */
    jl_init("/usr/bin");

    /* run julia commands */
    jl_eval_string("print(sqrt(2.0))");

    /* strongly recommended: notify julia that the
         program is about to terminate. this allows
         julia time to cleanup pending write requests
         and run all finalizers
    */
    jl_atexit_hook();
    return 0;
}

What you need is the Julia header files and Julia runtime(libjulia.so).
From the other side, calling C in Julia is possible, too. Let's quickly look at Julia's doc. Julia supports an API called ccall to call C functions.

1, 2, 3...Rabbit!

What about other languages? I mean, how can other languages work well with Julia?
Some languages are implemented in C. Therefore, binding them with Julia is possible, even the binding is duplex.

Let's assume this language is X. What you need is to convert X's object into C struct, and pass it to Julia. Julia will convert it into Julia object, run Julia code in its runtime, and then pass C struct as result. Finally, X receives result and converts it back to X's object. Another question, what about GC? In Julia, you can handle objects' references, also, there is other ways to play with GC, like storing objects into global scope.

The supposition above has been implemented, the X is Python. This is what PyJulia and PyCall.jl do.(Though there is some problems with them, admittedly) Now, it should be time for Ruby.

C and more - LLVM

I have a plan. We need a mole in Julia, which handle Ruby object and Julia object, to take advantage of Julia runtime. Then, we need a Ruby gem, to wrap the (possibly dirty) details, let's name it Rumeo here. These two good friends will work like PyJulia and PyCall.jl.

What about going further? Julia is depend on LLVM, which supports its affinity for C. And Julia will be compiled into LLVM bitcode before into machine code. Hence, we can combine Ruby with Julia even in LLVM level. There is an implementation of Ruby working on LLVM, called Rubinius. We can write a gem with Rubinius, and communicate with Julia in the level of bitcode instead of C. It will benefit on speed, which makes thing better. This is why I call it fast bindings.

Drawbacks

Of course, this plan is speculative. It will be an experiment, a venture investment. Here list the possible drawback, you need to read them carefully before doing any decision:

  1. GC. Yes, there are some methods to handle GC, but we need to find a correct way.
  2. Julia's bitcode. Julia doesn't offer a way to generate its pure bitcode(code_llvm doesn't really generate pure bitcode), so, a hack can't be avoid.
  3. Rubinius wrapper. Rubinius offers a VM to run your code. So I have to wrap the bitcode in Ruby object, this requires well understanding on Rubinius VM.

Tentative Timeline:

Until May 25 (Community Bonding Period)

Reading docs and code of Rubinius and Julia. Try to find a way to hack. And read the code of PyCall.jl and Pyjulia. Do some design and experiments.

May 25 - June 24

Implement RbCall.jl.

June 25 - July 25

Implement Rumeo

July 26 - August 20

Implement Rumeo-llvm

August 21 - August 28

stop coding, start paperwork

Do you have other obligations from late May to early August (school, work, etc.)?

There will be some exams and some schoolwork in June. They will take some time, but could not be a problem.

About Me:

  1. I have been contributing to open source projects since two years ago.
  2. During the period of working in SCUT High-performance Computing Lab, I played a major role in the development of an application working in Tianhe-2 - the world's fastest supercomputer.
  3. I have experience in writing bindings for Python and Ruby(via FFI). I am also comfortable with Ruby and some other languages, such as C++, Python.
  4. I like learning new programming languages. It's fun to discover a new world.

The patches I sent:

只有神大人知道的世界

诶,《只有神知道的世界》真得太好看了!我一连花了两周时间,实现了12×3集TV动画加3集OVA、两百多回漫画的补完,打破了之前的补番记录。很久没有见过这么对我口味的作品了。

《只有神知道的世界》是关于Galgame之神——桂木桂马在迫不得已的情况,活用二次元Galgame知识攻略三次元妹子的(后宫向)故事。听起来是不是很像YY小说的剧情……囧。但其实这部动漫三观到是很正(不,我不是说人渣主角最后被正义的柴刀制裁的结局)。没有攻略之神桂木桂马攻略不了的妹子——前提是该妹子是二次元妹子,对于三次元妹子,神大人可是不感兴趣。BTW,攻略这个词被用于三次元中的把妹,也是因为神大人而起的。然而桂木桂马不犯桃花,桃花也会来找他。有一天,桂马被连蒙带骗,成为了来自地狱的Bug魔艾露茜的协力者,不得不用恋爱的方式驱逐寄居在少女心中的驱魂,否则就会人头落地。当然啦,为了避免Nice Boat的Bad end,根据设定,只需要接下吻就能驱逐驱魂了。而且驱魂被驱逐之后,少女的记忆会被消除。(然而这个设定是有问题的……这导致了之后剧情的暴走,以及修罗场)

于是心中只有Galgame的神大人,走上了开后宫 成为人渣 拯救失足少女的不归路……

已经踏上了不归路的神大人,不情不愿下被命运的洪流所推动,跟一位位少女上演恋爱的轻喜剧。于是乎,我们看到神大人将Galgame的知识活用到现实世界中,跟艾露茜一起攻略各路妹子,在经历千辛万苦后终于解开妹子的心结,吻得美人归。虽说是“经历千辛万苦”,但是期间不乏各种笑点,以及神大人的各种机智点子,令人不知不觉中喜欢上神大人这个心里只有Galgame,却不得不去攻略 拯救失足少女的家伙。每当少女解开了心结,却忘却了跟神大人共同的记忆,只有神大人带着美好的回忆,继续他在Galgame的征途时,我总是替神大人感到可惜。

然而爱开玩笑的命运,并不允许神大人一幕幕地拍恋爱轻喜剧(哦不,我们不应该怪罪爱开玩笑的命运,应该怪罪脑洞大过天的画家)。神大人在完成一次次攻略后,逐渐接近10年前驱魂大逃脱的真相。于是为了呃,宇宙的爱和和平,不,为了三界的未来(确实如此),神大人开始走上一条成为人渣的不归路。为了集齐六位女神封印驱魂,神大人决定放下心爱的Galgame,心爱的二次元女神们,要从过去攻略过的妹子中寻找女神,而所拥有的线索是,心中栖息着女神的妹子,不会忘记跟神大人的共同回忆。也就是说,嗯,神大人要去问妹子们,你们记得“大明湖畔的桂木桂马吗?”,对了,这次是需要同时攻略七人哦!虽然神大人可以同时打六个Galgame。然而Galgame是Galgame,现实是现实,如果有谁敢现实里同时攻略两个妹子,未免会被打上“脚踏两条船”的印记,然后被众人所唾骂,而神大人的目标是,七个……这次就算五体投地也踏不上了。难怪神大人说,他决定变成攻略之鬼(攻略之人渣,即鬼也)。喔,我已经看到结局了,人渣诚在彼端向神大人你招手。

当然了,神大人这么做,也不是他一时心血来潮。毕竟,这一切都是命运的逼迫。如果没有被当作协力者,神大人乐得生活在Galgame的世界里。那里有他心爱的二次元,有他的四叶……“我确实对现实绝望了,但是我没有对自己绝望,决定现在平凡与否,开心与否,快乐与否的不是现实,而是我,我确实对现实绝望了,但是我没有对自己绝望”。神大人,其实是个生活在二次元里的现充。然而没办法了,面对如果不去做,就会有人牺牲的困境,神大人勇敢地面对命运的挑战,踏上了成为攻略之鬼的道路。其实,我觉得还有一个原因,神大人对现实世界还是不怎么习惯(没有那么多**负担和道德约束),所以能够总是保持冷静,去找寻通往结局的最短路线。

然而这次,神也失手了。现实世界比Galgame复杂得多,神大人发现千寻是真心喜欢他,而不是因为受到有意为之的攻略的影响。不过,千寻心中没有女神,没有女神,也即不是攻略的目标。既然不是目标,就应该果断拔flag,然而,怎么能这样对待一个真心喜欢自己的人呢……最后的选择是无奈的,以至于接下来的剧情犹如暴走。然而局势已经不容婆婆妈妈了,神大人没有别的路线,只能一条路走到结局了。于是怀着对千寻的歉意,神大人终于集齐了六个女神,解除了正统恶魔社的威胁。

不过神大人的日常生活(天天无时无刻打Galgame)还没来得及恢复,新的挑战(命运的逼迫!)又过来了。为了保证打败正统恶魔社的事情确实能够发生,桂马被女神们传送回10年前。这次他需要修正世界线,让某一个特定的未来(成功的未来)能够发生。而他能够做到的,就是跟Bug魔艾露茜和新的助手一起,继续靠攻略妹子拯救世界。经历了重重困难,包括跟正统恶魔社的恶魔斗智斗勇,跟恶魔女王香织见招拆招,以及跟青梅竹马天理的羁绊,神大人终于修成正果,让过去的世界线接上自己熟悉的世界线,也拉开了《神知》故事的大幕。于是一切从10年前开始,攻略妹子也要从小计划啊。

就像所有别的故事一样,最后的结局里,正义都战胜了邪恶,神大人“胜算不是什么时候都有的,就算没有胜算我也要做给你看”,终于在“狗屎游戏般的现实中”,获得了“只有在游戏中追寻到的理想结局”。然而跟别的故事不同的是,勇士没有跟公主走在一起,“我们之间是不可能的”,虽然经过了10年的羁绊,桂马和天理还是没有走到一起。最后的结局,是属于千寻和桂马的。虽然无论天理还是千寻成为最后的女主角,我都没有所谓啦。但是看到这样的结局,还是感觉有点儿失落。毕竟,天理的戏份比千寻重多了,让天理成为最终的唯一更加众望所归是不是……不过神大人还是选择了千寻,毕竟千寻的反应总是能让他措手不及,让他体会到爱的感觉,也许,这就叫爱情吧。

在我的心目中,神大人是一个让我敬仰的存在。不是说攻略妹子的能力举世无双啦……神大人整天打Galgame,别人都嘲笑他是眼镜宅,然而神不在乎。他不是为了逃避现实而沉迷于Galgame,而是因为Galgame中有理想的结局,而现实只是一个狗屎游戏。虽然如此,当身陷狗屎游戏时,他总是全力以赴,极其冷静地去找寻通往结局的路径,就像每个孜孜以求的Galgame玩家一样。虽然本人对Galgame并不感冒(只对Galgame改编的动漫感兴趣哈),但是他的心态和行动,却也鼓舞着我这个字面意义宅,为了更加理想的结局而战斗,去找寻现实生活中的flag,去看到那个理想的结局。

用各种编程语言实现同样的调用

最近几个月的空闲时间里,我一直在做一件事,就是用各种编程语言实现同样的一套“算法”。
虽然名为“算法”,其实指的是C++标准头文件algorithms和numeric里面的一套辅助函数。因为这部分内容一般情况下都称之为STL算法,所以我也可以大胆地宣称自己实现的也叫做“算法”,哈哈。
目前,已经完成了C++/Python/Javascript/Coffeescript/Java这几个版本。由于新学期会有新的目标和计划,所以这个小实验就此打住,估计将来也就这几个版本。算法的时空效率方面,也许倒不是每一个都能遵守。代码实现方面,也更看重于代码是否清晰优雅,而不是效率(毕竟,如果要写出一个高效率的实现,恐怕得死更多的脑细胞)。


聊聊下自己的感受:

第一个版本是C++实现的,用C++实现自带的辅助算法。由于要实现的本身就是为C++量身定做的内容,算法的参数和功能无须作出任何裁改。大部分函数都没有难度。
但是,诸如stable_sort, rotate, prev_permutation, make_heap之类的函数,还是花了一些功夫去思考。

第二个版本是Python实现的。这个版本的实现很快就遇到了一个问题,Python里面的iterator和C++的iterator不是同一个东西。
在Python里面,只有一种iterator;而C++中有各种iterator,输入的/双向的/随机的,等等。
可以这么说,在C++里面,所有可以访问容器中的数据的方式,都算作iterator。而在Python(和其它语言)里,iterator指的是一种在实现了__iter__(或者叫别的什么东西)的接口的容器中迭代访问的对象。
Python里面,只是单单实现了__iter__的对象是不能修改里面的值,即相当于C++的input iterator,你只能拿来input,其他的事情一概不能做。
然而相关的算法中,大部分需要使用Forward iterator。这种iterator允许你在迭代的时候修改当前迭代到的对象。
还好Python里面可以找到差不多的对象,只要该对象实现了__setitem____getitem__即可。严格来说,这算是random iterator的要求。
所以说,这个差不多真是差好多啊。

因此,许多算法在设计的时候,就只能应用在list上,尽管它们并不需要随机访问的要求。

有了C++和Python版本,其他语言的版本就方便很多了。因为可以直接借鉴C++和Python的实现……

第三个版本是Javascript实现的。写了一会儿,发现Javascript跟Java还是很像的,都挺罗嗦。基本上,Python可以找到语法糖,三言两句就能解决的问题,要是使用了Javascript,你就不得不写i < xxx.length; i++....blabla,尽管它们都同为动态语言。写的时候我就猜了下,Javascript版本一定会比Python版本的要长得多,甚至说,比起静态类型的C++,Javascript版本也短不到哪里去。事后看来,确实如此。
而且让人讨厌的是,Javascript函数最后时不时要带上});这样的一串尾巴。处处带花括号就算了,你还要带上小括号,看来JS受C和LISP的影响真得挺深。

第四个版本是Coffeescript写的,Coffeescript不愧是Javascript的加糖版本,一下子就简单很多。写完这个版本,再一次坚定了从Javascript转到Coffeescript的决心。
当然,Coffeescript只是在JS上撒了薄薄一层糖霜。JS里没有的类库,在Coffee里面也没有……甚至说,JS的一些怪癖,Coffee也不能免除。你还是要小心翼翼,才能全身而退。

最后的一个版本是用Java写的。


事后我统计了各语言实现版本使用的行数:

  • c++ : 2145
  • python : 1042
  • javascript : 1724
  • coffeescript : 1269
  • java : 2185

可以看出,Javascript版本的确够罗嗦的,都快赶得上Java和C++了。Coffeescript虽然写起来轻松,但还是比Python的要长,这出乎我的意料之外。
另外Java的长度仅仅比C++的长一点,原本我还以为Java版本会比C++的长得多呢。看来我对Java的偏见,是时候改观了。

MiniTest的正确使用姿势

MiniTest是什么?不懂的请搜索一下,我就不解释了。

在MiniTest之前,用Ruby做测试的有两种人,一种人喜欢Test::Unit的test_*风格,另一种人喜欢Rspec的describe风格。他们时不时因为这两种风格的优缺点以及哪一方才能代表真正的Ruby测试风格而争执不下。(这有点像《格列佛游记》中,两个小人国因从哪个方向打碎蛋壳而反目成仇)

后来,MiniTest出现了,改变了这一切。MiniTest向众人宣讲道,“汝可择Test::Unit之道而从之,亦可择Rspec而从之”。在MiniTest里,你可以像Test::Unit那样写测试,也可以像Rspec那样写测试。是故,MiniTest用一种包容的心态解决了纷争,重新给世界带来了和平。

这篇文章,就是讲讲MiniTest的正确使用姿势。

Hello World

使用了MiniTest的测试代码像这样:

require 'minitest/autorun'

# 注意有些资料中,测试类不是继承自MiniTest::Test,
# 那是MiniTest 5之前的做法,MiniTest会通知你改正
class TestMyLife < MiniTest::Test
  # 这个方法会在各个测试之前被调用
  def setup
    @me = People.new
  end

  def test_sleep
     # assert_equal exp, act, msg
     assert_equal   "zzZ", @me.sleep, "I don't sleep well "
  end

  def teardown
  end
end

MiniTest中可用的断言(assert_*)有很多个,具体可以看看文档:
http://docs.seattlerb.org/minitest/Minitest/Assertions.html

另外,在每个test方法之外,还可以执行被称为lifeCycleHook的方法,比如前面的setupteardown
具体的顺序为:

  1. before_setup
  2. setup
  3. after_setup
  4. test
  5. before_teardown
  6. teardown
  7. after_teardown

看到这里你多半会觉得,有setupteardown就够了,一大堆别的before/after还有啥意义啊!文档也提到了,一般用户只需用到setupteardown就够了,其他的hook是给MiniTest拓展用的。

前面说了,MiniTest兼容并收,即可用Test::Unit风格,也可用Rspec风格,下面就改用Rspec风格写上面的例子:

require 'minitest/spec'
require 'minitest/autorun'

describe "TestMyLife" do
 before do
    @me = People.new
  end

  it "test_sleep" do
     assert_equal   "zzZ", @me.sleep, "I don't sleep well "
  end

  after do
  end
end

看出不同了么?Rspec风格就是用describe替换掉class,用it替换掉测试方法,就是把对象风格的测试用例,变为了Rake这样的DSL了。注意前面要添加require 'minitest/spec',让MiniTest知道现在是Rspec风格的测试。

事实上,在Rspec风格中,有人会追求用Expectations代替Assertions,就是写

@me.sleep.must_equal("zzZ", "I don't sleep well ")

而不是

assert_equal   "zzZ", @me.sleep, "I don't sleep well "

不过这个看个人的口味啦。如果你对此有兴趣,看看这个MiniTest的cheet sheet:
http://danwin.com/2013/03/ruby-minitest-cheat-sheet/

运行测试

哎呀,啰啰嗦嗦说了一大堆,好像没讲明白如何运行这些测试呢。

其实很简单,直接用ruby运行吧。ruby test_file.rb即可。如果要运行多个测试,写一个脚本好了。
不过大家都是用rake来运行测试的,事实上rake也集成了运行测试的功能。

看看下面的Rakefile片段:

Rake::TestTask.new(:test) do |t|
  # libs表示要添加到$LOAD_PATH(就是加载ruby文件的搜索路径)的文件夹
  # 默认是"lib",现在再添加"test"
  t.libs << "test"
  # 要运行的测试文件的特征。匹配以test_开头的所有文件
  t.pattern = 'test/**/test_*.rb' 
  # 不输出测试文件的信息
  t.verbose = false
end

然后运行rake test就能运行测试了。如果你设置task :default => :test,那么仅需rake就能运行默认的任务 - 测试了。
BTW,通过指定rake test TEST=xx,还可以只运行指定文件哦,当一个项目中包含的测试比较多时,只运行相关文件能省下许多时间。

进阶功能

除了前面提到的常用功能,MiniTest还提供了其他进阶功能,比如:

Benchmark

还是用代码做介绍吧。

require 'minitest/autorun'
require 'minitest/benchmark'

def setup_array(n)
  return Array.new(n){ |i| i }
end

# 需要继承自Benchmark类
class TestBenchmark < MiniTest::Benchmark
  # 所有函数以bench_开头
  def bench_algorithm
    validation = proc { |x, y| x.each_index do |i|
      puts "#{x[i]}\ttime cost: #{y[i]}"
    end
    }
    assert_performance validation do |n|
      ary = setup_array(n)
      100.times do
        ary -ary
      end
    end
  end

  def bench_constant
    # 常数
    assert_performance_constant 0.9 do |n| 
      ary = setup_array(n)
      100.times do
        ary.length
      end
    end
  end

  def bench_logarithmic
    # 查找时间复杂度与其说是log的,不如说是n
    assert_performance_logarithmic 0.9 do |n|
      ary = setup_array(n)
      100.times 
        ary.find 10001
      end
    end
  end

  def bench_linear
    # 线性
    assert_performance_linear 0.9 do |n|
      ary = setup_array(n)
      100.times do
        ary.sort!
      end
    end
  end

  def bench_power
    # n^2
    assert_performance_power 0.9 do |n|
      ary = setup_array(n).shuffle!
      100.times do
        ary - ary
      end
    end
  end

  def bench_exponent
    # 指数
    assert_performance_exponential 0.9 do |n|
      100.times {2 ** n}
    end
  end
end

MiniTest支持Benchmark的测试,你可以测试函数的时间复杂度。这些断言接受两个参数,一个是精确度,另一个是要运行的block。当然你也可以自定义精确度的计算方式,正如第一个bench_algorithm所示。(虽然在那里我只是把输入和运行时间打印出来。)另外你也可以自定义参数的范围,默认条件下是从1到10000,按10倍增长。

用Rspec风格写Benchmark也是可能的,具体看文档。
http://docs.seattlerb.org/minitest/Minitest/BenchSpec.html

Mock

有些时候,测试代码中可能会有这样的调用:它们消耗大把大把时间,占了单元测试的大部分时间;或者会带来不可逆的副作用,比如往远程数据库中添加数据,而你又不能每次执行都清空数据库。
这时候,我们应该怎么办?把调用标记为“FIXME”,然后交由维护的程序员头疼去?
在软件测试中,我们可以使用Mock来解决这个问题。我们不需要真的做这个调用,而是返回一个预期的值,交由要测试的方法进行处理。这样,我们无需进行完整的调用,就可以以合理的输入来测试方法。

MiniTest也集成了Mock的功能。一如既往,我还是直接show you the code:

require 'minitest/autorun'

class TestMock < MiniTest::Test
  def setup
    @badman = MiniTest::Mock.new
    # expect :method_name, retval, args=[]
    #@badman.expect(:destroy_my_computer, true, [String])
    @badman.expect(:destroy_my_computer, true)
    @goodman = MiniTest::Mock.new
    @goodman.expect(:destroy_my_computer, false)
  end

  def test_mock
    assert_equal true, @badman.destroy_my_computer
    assert_equal false, @goodman.destroy_my_computer
    #assert_equal true, @badman.destroy_my_computer('brutally')
  end

end

所有Mock的实例都有一个expect方法,接受:method_name, retval, args这三个参数。其中args是输入参数数组,表示允许的输入范围。而retval是对应的返回值。

MiniTest还有其他一些功能我没有介绍,比如允许第三方插件自定义test reporter等等,如果需要,就阅读文档 + 搜索一下吧。

MongoDB, no SQL injection?

最近发生一件事, Ruby-China Mongodb注入可导致盗用管理员(他人)身份发帖引起了我的兴趣。

具体内容可以移步到链接去看。随便给出对应的pr地址:ruby-china/homeland@ff19cc1

于是我搜索了相关资料,以搬运工的身份写下这篇文章。

MongoDB, no SQL injection?

有人的地方就有江湖,有DB的地方就有injection。SQL数据库如此,No SQL数据库亦是如此。考虑到No SQL数据库用的不是SQL,这里使用SQL injection是否有点不太恰当?不过总不能说是No SQL injection吧XD。

言归正传,OWASP上面有一篇文章, https://www.owasp.org/index.php/Testing_for_NoSQL_injection, 讲到MongoDB的注入问题。另外,MongoDB官方文档FAQ中也提到对于SQL injection的防范方式。其他地方提到的资料基本上大同小异。

根据上面两份资料的内容,以及其他在网上找到的内容:

  1. No SQL不代表安全。由于No SQL使用的查询语言很多是过程式语言而非声明式语言,No SQL注入的危害甚至比传统的SQL数据库更大。
  2. 不要草率地通过拼接用户输入的方式来生成查询语句。这个就不需要举例吧,了解MongoDB查询语言的人,应该可以想象到会是什么结果。
  3. 好消息:MongoDB会在driver中将查询字符串编码成BSON格式。类似于,:{这样的奇奇怪怪的符号会导致BSON构造出错。这样cracker在注入时需要花费更多的心思了。
  4. 坏消息:$.不在上述符号之列。
  5. 又一个好消息:Mongoose宣称,为了提高安全性,它会根据Schema来强制转型输入的内容,因此像$xxx这样的变量会被转化成预定义的类型。如果你还是放心不下,参照 http://stackoverflow.com/questions/15917400/how-dangerous-is-a-mongo-query-which-is-fed-directly-from-a-url-query-string 里面的回答,自己写验证函数。
  6. 最后是坏消息。在下列四个函数中,你可以直接传递字符串作为参数。记住它们的样子,见到它们可以高呼“Eval is evil!”

具体是干什么的,请参考对应文档。

注意$where,这个跟MongoDB ORM(姑且这么称呼)中提供的where调用不一样。这个$where可以接受任意查询表达式,并且返回其结果,相当于特殊的eval。其中Mongoose也特别提到了它:

  • ####NOTE:
    *
  • Only use $where when you have a condition that cannot be met using other MongoDB operators like $lt.
  • Be sure to read about all of its caveats before using.

https://github.com/LearnBoost/mongoose/blob/master/lib/query.js

其实,Ruby China事件其中的主要问题跟MongoDB注入无关……

前面说了一大堆,关于MongoDB注入的问题,但是Ruby China事件的主要问题,跟MongoDB无关……(XD请各位看官先吸口气)

好了,主要的问题是:param的值一定是字符串么?

回到Ruby China的pr上来,这个pr主要是新增

token = params[:token] || oauth_token
+
+      # 防 mongodb 注入
+      token = token.to_s

@current_user ||= User.where(private_token: token).first

显然,token的值不一定是字符串!那么,这个引发了血案的token原本是什么值呢?

我特意用Rails测试了一下:
(注意,因为Rails是通过Rack来解析HTTP参数的,而基本上Ruby Web框架也都是使用Rack,所以下面的结果应该适用于其他Ruby框架)

...
p params[:page]
...

http://localhost:3000/page[$gt]=1
输出{"$gt"=>"1"}

现在大家知道传给User.where的token到底是什么值吧!

继续测试:
http://localhost:3000/page=[1,2]
输出[1,2],嗯...

继续继续:
http://localhost:3000/page=1&page=2
输出2

限于时间和精力,我只测试了Rails的情况。不过根据那篇乌云文章所言,PHP和Node对于HTTP参数的处理方式也跟Rails差不多。

其实,这种攻击方式并不新鲜。它甚至有一个学名,叫做HTTP Parameter Pollution,简称HPP。原理就是利用不同的服务器和服务端框架对HTTP参数的处理方式不同,提交奇奇怪怪的HTTP参数,来绕过逻辑判断。

所以说,搞了半天,最后还是回到一个老问题上了,永远不要相信用户的一切输入!

总结

  1. No SQL也会有SQL injection。
  2. 最好不要拼接查询语句,因为漏洞什么的,你永远也想不到会从哪里冒出来。
  3. 注意MongoDB查询语句中的危险分子。
  4. 永远不要相信用户的输入。无论什么输入,都要在服务端程序中过一下,哪怕是转化成字符串这样的操作也好。

当我谈vim映射时,我谈些什么

映射功能是当下各大编辑器的标配,如果你想要熟悉所用的编辑器,必然不能缺少对它的映射机制的学习。对于vim亦是如此。

这里说到的映射功能,指的是编辑器会捕获用户的输入,并且按照事先的设置来执行某些动作。

基础

在vim里面自定义一个映射,格式如下:

maptype key action

如:

inoremap jk <c-[> " 在insert模式下映射jk为Ctrl+[,也即进入normal模式

maptype表示映射的类型,分为两大类,带nore的和不带nore的(具体意义稍后再谈)……每一类中,根据映射的可用范围再分成若干类,具体类型通过:help map-overview可以查到。这里列举下重要的几类:

  1. map: 在所有模式下可用的映射
  2. vmap:在visual和select模式下可用的映射
  3. nmap:在normal模式下可用的映射
  4. imap:在insert模式下可用的映射
  5. omap:用于motion的一部分的映射。比如vw就是visual模式下选中一个词,可以用omap定义类似于w这样的动作操作符。
  6. cmap:用于在命令行下(输入:/之类后)可用的映射

key表示映射的键。什么样的键可以被映射呢?基本上你在键盘上能看到的键都能被映射(实际情况并不如此理想,等会解释)。如果你想映射特殊的键,比如 ,可不能就直接打个 上去f,而要使用<space>来表示。各种特殊符号具体的表示方式见:help key-notation。注意不及能映射单个键,还能映射一组键,比如noremap afhaso; 脸滚键盘

action就是映射出来的动作。可以是一串字符串,或者调用一个函数,还可以是调用一个vim命令。这个就要看大家的想象力了。

进阶

从这里开始就要举出更多映射的例子啦。

Notice! 不要说“为什么要这样映射,XX键本来有YY功能,这样做不对”之类的话,毕竟这个是关乎personal taste的事情。这里提醒下,在映射一组键之前,先看下这个键是不是已经有默认的功能了,然后看下这组键是否被映射了,再来决定要不要映射它。否则等到已经习惯后,一旦想要改,也没那么方便了

通过:help命令查看某组键是否有系统默认功能。
通过:map命令可以显示当前键映射的情况。

noremap VS map

noremap表示不允许映射的结果参与其他的映射规则的匹配。而map会使得映射的结果可以继续匹配其他的映射规则。

举个例子:

nnoremap ; :
nnoremap : ;

这里把;和:两个符号互换了,因为在normal模式下,:用到的频率比;高。假如这里用到的是nmap呢?那会导致vim卡上一段时间,直到你按下Ctrl+c或者抛出个错误。所以基本上都是用noremap作为映射。

当然map也有用武之地,比如当你需要映射的结果来触发另一个映射时,就用得上map了。

cnoremap Or command

cnoremap会在命令行里起作用。
试一下输入::cnoremap w!! w !sudo tee >/dev/null %
然后敲:进入命令行,快速地敲出w!!,你会发现它展开成为w !sudo tee >/dev/null %。这就是cnoremap的效果了。

用cnoremap可以大大缩短常用命令的输入时间。举个例子,你可以使用cnoremap UE UltiSnipsEdit来代替敲入整个命令(或者多次敲打tab键)。不过前提是你的手速要足够快……

其实为什么不用command呢(现在:help command看看)。你可以用command命令给某个命令做别名,这样就不用依赖足够快的手速了。

can map and can't map

前面说过,实际上不是所有的键可以作为vim映射的键。这是因为要想触发vim映射,你要让vim捕获到某一组键才行。但是有些键不会被传递给vim,可能半途就被其他程序偷吃掉了。这种情况在终端vim下特别明显。因为终端会占用一些快捷键,而且有些特殊的键值,比如shift+tab,即使终端它自己不用,也不给vim使用(好过分喔)。所以到底某个键能不能拿来做映射,还是得试了才知道。

脑洞大开

在这一部分,我来分享些自己觉得有用的映射,但求抛砖引玉。

" 切换鼠标模式和无鼠标模式。方便复制
function! ToggleMouse()
    if &mouse ==# 'a'
        set mouse=
        set norelativenumber
        set nonumber
        echo 'no mouse mode'
    else
        set mouse=a
        set number
        set relativenumber
        echo 'mouse mode'
    endif
endfunction

noremap <F2> :call ToggleMouse()<CR>
" 在安装了ag.vim插件后,查询光标下的内容
nnoremap <leader>sc :Ag! <cWORD>
" 在新的tab里编辑当前目录下的其他文件
nnoremap <C-down> :tabedit <c-r>=expand("%:p:h")<cr>/
" 快速开始一个全局替换
nnoremap <leader>s :%s///gc<left><left><left> 
" 编辑shell文件时,调用man命令查看文档
au FileType sh nnoremap <leader>m  :!man <cWORD><cr>

也许后端MVC的说法已经过时了

备份自:http://segmentfault.com/a/1190000004213733

呃,标题有点耸人听闻,不过我并不是标题党。考虑到谈论大而虚的东西(比如最好的语言)容易引起争论,所以还请诸君带着看戏而不是庭辩的心态来看待本文。

依我个人所见,后端框架,类似于MVC这样的组织方式已经显得过气了。

过去,在创建应用时通常会按MVC各建一个文件夹,每个文件夹就是一个模块。MVC三者的职责是这样的:

  1. Controller绑定到某个路由上,接着处理请求参数,然后创建在整个请求中可见的对象,并进行一些业务逻辑上的工作。期间会从数据库中构造Model,也有可能新建/修改Model后将它们保存入数据库。最后,Controller会通过View响应用户的请求,或者返回重定向的报文。基本上业务逻辑都是实现在Controller里面的。(下文为了阐述方便,都以“业务逻辑”特指Controller中的业务逻辑)
  2. 每个Model往往对应数据库的一个实体。Model类除了装数据之外,还提供了跟自己身份相关的一些方法,比如User类提供authenticate方法以用于验证密码。Model对象通常还会负责检验构造自己的参数是否正确。
  3. View负责使用给定的参数渲染模板,并作为响应返回给用户。View一般是由该框架提供的模板语言写成。

现在,一个后端应用如果还是按MVC的方式划分,似乎有些不适应了。

后端MVC的历史

我们先从后端MVC的历史开始讲起吧。MVC最初发端于客户端程序的开发,旨在用Controller在Model和View之间进行解耦。如果我没记错的话,gang of four的《设计模式》里面提到MVC,就是以客户端程序作为例子。

跟客户端开发类似的,后端程序一开始也只有View这一层。在很久很久以前,后端程序都是这样运行的:用户一个HTTP请求进来,服务器通过cgi调用一个程序生成文本作为响应。这时期大部分后端程序,看上去都像是模板语言(见过初学者写过的JSP/PHP吗?)。因为它们主要做的事情,就是从用户输入和数据库中获取数据,并拼接字符串生成文本。后来后端程序开始演化得越来越复杂,单单一层View已经不适应了。由于需要把逻辑从View中分割开来,后端程序开始走上客户端程序走过的路,进行MVC的分离。于是,负责路由和业务逻辑处理的部分变成了Controller,负责数据处理和持久化的部分变成了Model。

虽然后端程序也是做了MVC的拆分,但是它跟客户端的MVC其实是不同的。
在客户端里,Controller把View和Model分离开来,实现View和Model的解耦合。View上的变化,通过Controller传递给Model,然后再将Model最新的数据通过Controller传递回View。View:Controller:Model的比例通常是N:1:N,其中每个View基本对应一个Model。

然而,在后端程序里面,View和Model通常没有很强的对应关系。一般意义上的CRUD,基本上是Controller(业务逻辑)围绕着Model(数据层)在转。View扮演的往往是跑龙套的角色。

还需要V层吗

在客户端MVC中,View扮演的是跟其他二者三足鼎立的角色。用户的输入经过View,底层数据的变更通过View反馈给用户。

然而后端MVC中,View的地位摇摇欲坠、可有可无。前文提到,Controller绑定在路由上,接收请求;Controller渲染模板,发送响应。跟客户端不同的是,Controller只有在渲染模板时才用上View。View的戏份一下子被砍掉了一半。祸不单行,Controller并不一定需要渲染模板来发送响应,它可能直接就重定向了;或者更常见的是,Controller直接把一个对象JSON化,并把它响应给用户。这么一来,View的戏份还剩多少?

有些后端框架提供了JSON格式的模板,多多少少试图挽救View的没落地位。可惜并没有什么用。
过去,View通常由三部分组成:html代码,控制流程语句,渲染时的上下文。如果提供的是JSON格式的模板,那么View的前两部分基本不需要了,只需要渲染时的上下文。可是这么一来,为什么我还需要渲染一个JSON模板,直接用上下文的数据创建出个实例,由它生成JSON字符串,不也行吗?虽然我多了个用于响应的类,但是少了个JSON模板啊,而且说不定就不用给View留个文件夹。

最近我参与开发的几个后端应用,根本没有View的容身之地。所有的响应都是JSON格式,都是由特定的类JSON化出来的。
很多情况下,后端应用要么仅仅是大后端系统中的一个组件,要么需要跟多种来源的客户端打交道。通常它们只是作为数据的守护者,API的执行人,仅响应以JSON数据。至于接收者想用这些数据做什么,那是它们的事了。也许是拿来渲染前端页面,也许是保存在客户端的数据库里,也许是拿去进一步分析处理。View已经退化到算不上一个层了。

M负责数据实体还是负责数据的访问

说完摇摇欲坠的View,接着说地位尴尬的Model。Model是数据的化身,后端开发千变万变,核心都是数据的处理。可以说,Model就是占了个风水宝位。不过在我看来,当前常见的做法——只划分一个Model包——并不够清晰。

以我愚见,后端程序中的Model其实做了两件事。一件事是表示了数据实体,另一件则是负责数据的访问。按照单一职责原则,Model这样一身饰两角是不对的。数据实体是一回事,对应的数据实体的访问是另一件事,两者不能混起来。

假设保存Account需要一个事务,在这个事务里面要更新AccountBalance两个实体。下面是Rails里面的做法:

# always save Account in a transaction
Account.transaction do
  balance.save!
  account.save!
end

问题是,这段代码应该放到哪里?一个做法是放到Controller里面,但是保存Account的方式,不应该放到Account里面吗?另一个做法是放到Account类里面,但是为什么不放到Balance里面呢,这个事务也保存了Balance。作为程序员,在这件事上可不能偏心哦。
如果提供了DAO作为中间层,那么就不会这种“偏心”的顾忌了。而且这种带事务的保存,跟Account类自带的save方法的差异,也从层级上体现出来。

此外,Model层里面的类,不一定对应着数据库上的表。每个Model都知道如何持久化自身数据,这种假定是无法一直保持下去的。如果没有把数据实体和访问数据实体的组件区分开来,总有一天会陷入名不符实的危机中。

一个好的例子是,SQLAlchemy提供了Session类来完成对具体数据(Model)的访问操作(事务、保存等等),这样仅需稍加包装,我们就能分离出一个数据访问层出来,避免数据实体和数据访问间纠缠不清。

session = Session()
try:
    account = session.query(Account).get(...)
    balance = session.query(Balance).get(...)
    ... # 对account和balance做些修改
    session.commit()
except:
    session.rollback()

C:什么都往里装

调侃了View和Model,是时候对最后的Controller下手了。相对于View负责展示,Model负责数据,Controller的职责并不清晰。Controller是个筐,什么都可以往里面装。凡是无法区分到View和Model的,都放到Controller里面吧。所以,在MVC中,Controller往往是最臃肿的。

终于有一天,我们下定决心要整治下Controller乱七八糟的环境。一个通常的做法是,把某个路由上Controller的函数,拆分成若干个小函数。这些小函数不绑定路由,纯粹就是业务逻辑的抽象。拆分之后,Controller不再臃肿了,抽象出来的业务逻辑也可以被复用。
其实往更深一点思考,也许Controller本来就可以拆成两部分,一部分负责绑定路由,另一部分负责业务逻辑。

  • 绑定路由的部分,负责解决请求数据的完整性和正确性,及限流、鉴权等操作。在它的眼里,看到的是HTTP报文。
  • 业务逻辑的部分,负责具体业务处理。在它的眼里,看到的是用户的操作。

这样一来,业务逻辑的实现就跟路由绑定解耦合。我们可以给不同的路由提供一样的业务逻辑处理的同时,保持在限流等方面上的区别对待。我们也可以以此解决API设计上的遗留问题——旧的API,就让它们调用到新的业务逻辑上。

新的划分方式

  • View消亡了
  • Model分离成两层,一层负责数据实体,另一层负责数据的访问。
  • Controller分离成两层,一层负责绑定路由,另一层负责业务逻辑。

当线程遇到信号

信号是Unix系统中报告各类事件的机制。在设计信号系统的时候,还没有线程这个东西。Unix默认总是有一个进程来响应某个信号。所以当线程被加入到Unix系统后,如何处理信号就成了标准迫需解决的问题。

APUE中也提到了多线程时代下的信号处理问题,但是讲得比较简略。有些问题我还是弄不明白,所以就写了下面几个实验来验证下。

注,以下实验中创建和取消线程的代码从略

1. 同个signal是否发送到每一个线程

这部分代码很简单,就不贴出了。就是创建两个线程,然后在绑定的线程处理函数中输出tid。
测试结果,发现Linux对线程的处理是这样的,无论你创建了多少个线程,从头到尾都只有一个线程可以接收各种信号,其他线程都无法响应该信号。
只让某一个线程统一处理各种信号,这也正好是多线程程序处理信号的最佳方法。一般情况下,用户得使用sigaddser...等等来实现在别的线程中屏蔽所有信号,只让唯一的线程接收信号。
注意,即使在Linux下,总是只有一个线程可以处理所有的信号,我们也应该看到,这个线程的选择是随机的。所以,如果你不想某些工作线程受到打扰的话,你还是需要采用上面的“最佳实践”,在其他线程中屏蔽掉信号,只让选定的线程处理之。

ok,那么进一步说,假如我在某些线程中重绑定了信号处理程序,让它屏蔽掉某些信号,这时会发生什么事呢?

void rebind_sig_handler(int signo)                                              
{                                                                               
    printf("rebind %d in thread %lu\n", signo, pthread_self());

    if (signal(signo, sig_usr) == SIG_ERR) {
        perror("rebind signal failed");
        exit(1);
    }
}

// sig_usr中屏蔽掉某些信号。因为接下来会讲到,所以这里不说了。

实验证明,如果我们在默认的处理信号的线程中屏蔽了A信号,那么A信号就会交由剩下的未屏蔽A信号的线程处理。而且有趣的是,所有的信号都会转交给该未屏蔽线程来处理。假如该线程又屏蔽了B信号,而且总共只有两个线程,那么B信号会交由屏蔽了A信号的线程处理。

2. 某个线程重新绑定handler是否影响其他线程

前面演示的重新绑定的情况就已经显示了,当一个线程重新绑定handler时,其他线程也会一并受影响。

3. cond_wait状态下是否有signal丢失的问题

测试代码如下:

 pthread_mutex_lock(&mutex_second);                                          
 printf("thread %lu gets the lock\n", pthread_self());

if (pthread_sigmask(SIG_BLOCK, &blockSet, &prevMask) == -1) {
      perror("block signal failed");
      exit(1);
}

pthread_cond_wait(&cond, &mutex_second);
pause();
pthread_mutex_unlock(&mutex_second); 

结果:
所有的线程依然能正常接收信号

可以看出,cond_waitsignal不会丢失。具体情况如同调用了sleep一样。处于cond_wait下的线程会被短时间内唤醒,在处理完signal之后又继续cond_wait下去。如果在signal handler中调用了cond_signalcond_broadcast怎么样?在这里我没有测。

4. 因为pthread_mutex_lock而被阻塞的线程是否有signal丢失的问题

测试代码如下:
结果同第三项cond_wait部分。可见,pthread_mutex_lock阻塞下的线程不会有signal丢失的问题。

5. 如何在多线程下屏蔽某些信号

如之前的代码,使用

#include <signal.h>
pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

//sigset_t blockSet;
//sigset_t prevMask;
//sigemptyset(&blockSet);
//sigaddset(&blockSet, SIGUSR1);
//if (pthread_sigmask(SIG_BLOCK, &blockSet, &prevMask) == -1) {
//  perror("block signal failed");
//  exit(1);                                                                
//}

大体上的用法同sigprocmask()。据说在Linux上,这两个函数共享同样的实现。

// 解除屏蔽的代码则如下:
if (pthread_sigmask(SIG_SETMASK, &prevMask, NULL) == -1) {
  perror("reset signal mask failed");
  exit(1);
}

在多线程程序下的信号处理程序需要注意什么

  1. 遵循前面提到的只拿出单个线程来处理信号(为了实现同步接收异步产生的信号)
  2. 注意在信号处理函数中不要使用非线程安全的函数
  3. 除非做到了第一点,不要在信号处理函数中调用非异步信号安全(non-async-signal-safe)的函数。(包括了全部的Pthread函数) 这里有份异步信号安全函数白名单:http://docs.oracle.com/cd/E19455-01/806-5257/gen-26/index.html

将MySQL的默认Latin1连接改为使用utf8

最近惊奇地发现MySQL的默认编码方式居然是Latin1!而不是utf8!
于是即使我用的不是Windows,还是碰上了久违的编码问题……
错误是传入utf8字符(这里特指中文字符)后,MySQL报错说“奇怪的字符串,\xAC\x12...,不认识啊”。利用Python的decode,可以把这串报错字符还原成原本的utf8字符串。可见应该是MySQL的问题。
上网搜一下,得出是character变量设置的问题。这时候的character相关变量设定如下:

mysql> show variables like 'character%';
+--------------------------+---------------------+
| Variable_name            | Value               |
+--------------------------+---------------------+
| character_set_client     | utf8                |
| character_set_connection | utf8                |
| character_set_database   | latin1              |
| character_set_filesystem | binary              |
| character_set_result     | utf8                |
| character_set_server     | latin1              |
| character_set_system     | utf8                |

看来得把那两项latin1也同化成utf8才行。接着继续查资料。
看了下官方的相关字符配置文档,依然不知所云。然后看到一种做法,就是去改动/etc/mysql/mysql.cnf配置文件。改完之后,发现mysql无法重新启动了……呃,查看下/var/log/mysql/error.log,tail输出最后几行,发现刚刚添加的某个变量(default-character-set)是不合理的变量,所以报错了。遇到无法启动的问题,也过来查看下error.log好了。

最后是根据这篇资料改好的:

http://stackoverflow.com/questions/3513773/change-mysql-default-character-set-to-utf-8-in-my-cnf

果然爆栈网上啥都有。

转述如下:

 1. Remove that directive and you should be good. 

 2. Then your configuration file ('/etc/my.cnf' for example) should look like that:

        [mysqld]
        collation-server = utf8_unicode_ci
        init-connect='SET NAMES utf8'
        character-set-server = utf8

 3. Restart MySQL.

 4. For making sure, your MySQL is UTF-8, run the following queries in your MySQL prompt:

     - First query:

             mysql> show variables like 'char%';
     The output should look like:

             +--------------------------+---------------------------------+
             | Variable_name            | Value                           |
             +--------------------------+---------------------------------+
             | character_set_client     | utf8                            |
             | character_set_connection | utf8                            |
             | character_set_database   | utf8                            |
             | character_set_filesystem | binary                          |
             | character_set_results    | utf8                            |
             | character_set_server     | utf8                            |
             | character_set_system     | utf8                            |
             | character_sets_dir       | /usr/local/mysql/share/charsets/|
             +--------------------------+---------------------------------+

     - Second query:

             mysql> show variables like 'collation%';
     And the query output is:

             +----------------------+-----------------+
             | Variable_name        | Value           |
             +----------------------+-----------------+
             | collation_connection | utf8_general_ci |
             | collation_database   | utf8_unicode_ci |
             | collation_server     | utf8_unicode_ci |
             +----------------------+-----------------+

不过试了之后,character_set_database还是latin1。当然这个回答中的评论还是提及一点,对于已经存在的数据库,你还需要ALTER TABLE Table CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;来更改它们的字符设置。

我试了下这个命令,终于把最后一个变量也改为utf8了。**!不过……MySQL还是会报错!还是同样的错!不知怎么会有这样的问题……因为我最后一怒之下把数据库全部重新建过,就没有这个问题了!(当然这些都是开发用的数据库,随便drop下也无所谓啦)

为什么MySQL居然使用latin1作为默认编码呢……在解决了问题后,我特意去查了下。看来这个貌似是个遗留历史问题:

http://stackoverflow.com/questions/3936059/why-does-mysql-use-latin1-swedish-ci-as-the-default

真是的,凡是稍微大一点的东西总会有那么几个历史遗留问题~

C/C++ - 压榨结构体的空间

先给几个数据:

cout << "sizeof char is " << sizeof( char ) << endl;
// 1
cout << "sizeof short is " << sizeof( short ) << endl;
// 2
cout << "sizeof int is " << sizeof( int ) << endl;
// 4
cout << "sizeof bool is " << sizeof( bool ) << endl;
// 1
cout << "sizeof double is " << sizeof( double ) << endl;
// 8

好,那么问题来了,

struct Spot
{
    int x;
    int y;
    bool visible;
    int red;
    int blue;
    int green;
    double alpha;
    bool cleaned;
};
...
Spot spot;
cout << "sizeof Spot is " << sizeof( spot ) << endl;

输出的结果是多少?

这个问题对于写过C/C++的人来说,有点侮辱智商……好吧,不逗你玩了,直接进入正题。

输出的结果肯定不会是29(2 * 4 + 1 + 3 * 4 + 8)啦。都是Data structure alignment惹的祸。

Data structure alignment是个复杂的概念,简单来说,就是因为CPU访问内存时是成块成块读取数据的,所以编译器为了让CPU访问的时候更加方便些(同时也使得程序更加高效些),会将变量的内存地址移动到2的N次幂上。而为此空出来的空间叫做padding,在计算结构体总大小的时候,也得考虑这些padding。

那么怎么计算结构体的实际大小呢?由于C/C++是一门跟硬件密切相关的“底层”语言,这要看具体的硬件和相关的编译器实现了。这里给出一个可行的方法:http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/ 。简单说,假设CPU每次读取内存都是读4个字节,那么要把变量内存地址移到4的倍数上。不过在64位系统中,CPU每次可以读取8个字节,所以8个字节的double变量地址要位于8的倍数上。一个个变量算下来,最后就会得到结构体的实际大小了。

一个明显的推断是,如果改变结构体中声明的变量的位置,就能减少padding占用的空间。

记得有一个故事说,有个人想要填满一个杯子,他首先装上小石头,再装上一些沙子,最后倒入水,这时候杯子才真正满了。要想充分利用一个容器的空间,就要先装上体积较大的物体,然后依次装上体积稍微小的的物体。再来看下原来的声明:

struct Spot
{
    int x;
    int y;
    bool visible;
    int red;
    int blue;
    int green;
    double alpha;
    bool cleaned;
};

把各个变量按照从大到小重新排列,得到:

struct Spot
{
    double alpha;
    int x;
    int y;    
    int red;
    int blue;
    int green;
    bool visible;
    bool cleaned;
};

此时spot的大小仅为32byte,虽然还是有padding,但是已经跟原始大小差不多了。

还能进一步压榨么?

如果要想进一步压榨,就要从变量大小上打主意了。举个例子,这里的redbluegreen也许不需要使用int类型,xy也是同理,alpha可以使用float来代替。假如这些都成立,那么可以精简为:

struct Spot
{
    float alpha;
    unsigned short x;
    unsigned short y;    
    unsigned short red;
    unsigned short blue;
    unsigned short green;
    bool visible;
    bool cleaned;
};

对于热衷于压榨每一点资源的人来说,还是有一点不满意的。也许red,blue,green也就是0到255的范围,不过是2的8次方。那么用bool类型来代替呢?这却有一个问题,为什么一个表示颜色成分的变量是bool类型呢,这个除了大小,跟bool类型没有半点关系。类型语义上说不过去啊。

C++有一个语言特性解决了这个问题:Bit field

我们可以在声明变量的时候,给对应的变量指定一个bit field大小(单位是bit),改变该变量默认占用的内存大小:

struct Spot
{
    float alpha;
    unsigned short x;
    unsigned short y;    
    unsigned short red : 8;
    unsigned short blue : 8;
    unsigned short green : 8;
    bool visible;
    bool cleaned;
};

注意不能对bit field使用sizeof,编译器会报错。

嗯,虽然由于Data structure alignment的原因,spot的整体大小并没有改变,不过相关变量所占用的内存的确减少了。而且这种减少,并没有导致变量访问速度上的影响。
但是如果更进一步,把visible等bool类型变量变成大小为1 bit的bit field(我知道肯定有人迫不及待地想这么做),就会影响变量访问速度,因为现在bool变量已经不能凑整了。实验证明,速度有慢10%左右。

不如看下具体生成的汇编代码:

// size.cpp (without bit field)
...
spot.blue = 3;
...

然后……

$ g++ -g ./size.cpp
$ objdump -S --disassemble ./a.out > before
$ g++ -g ./size_with_bit_field.cpp
$ objdump -S --disassemble ./a.out > after
$ vimdiff before after

你会看到使用bit field之后,读取blue的指令是移动一个字节,而且取址的位置也不一样了。

换把bool类型变成bit field试试,这时候会发现,每次在访问bit field时,都会额外加多两条指令。因为这时候我们只需要取一个bit的内容,所以不能直接移动单个字节,这就导致了速度的下降。

我要上谷歌

Google终于彻底完蛋了……现在的问题是,即使在教育网下,GFW还是会干扰google.com.hk的连接。在Chrome下,会出现如下的SSL错误:

您与 www.google.com.hk 之间的安全连接目前正受到干扰。

请等待几分钟后再尝试重新加载网页,或在切换到其他网络后重新加载网页。如果您最近曾连接到新的 Wi-Fi 网络,请先登录再重新加载。

查了下,这就是所谓的“中间人攻击”,在GFW可能采取的手段中已经算轻了,不知将来会不会使用更那啥的方式。
不过通过google.com.de(德国骨科谷歌),还是可以访问Google的搜索服务。但是Chrome浏览器的搜索框会自动去使用google.com.hk的网址,即便你已经设定默认搜索引擎为google.com.de(真是不知为何)

这是在教育网中的情况。在外网里,自从6月以来,Google已经沦陷大约3个月了,这个纪录超过以往的任何一次封锁,甚至让人觉得Google已步Facebook等网址的后尘。
在外网里,我一直使用search.aol.com来作为替代,因为它是Google的马甲("enhanced by Google")。除了搜出来的内容是全英文,以及没有办法根据用户信息来个性化搜索结果以外,这个网站算是Google的完美替身。不过前几天会偶尔上不了。而且它的知名度越来越高,恐怕……

如果这些网站都上不了,那我只好退而求其次,选择bing或者duckduckgo。

我为什么要千方百计去访问Google呢?因为其他的搜索引擎都不如它(如果不是远远不如的话)。

bing中太多无效的、肤浅的信息,而且对热词处理的不好,只要你的查询语句中存在那么一个热词,搜索出来的内容明显就是偏向这个热词的。然而,热词一般都不是我们想要的结果,只是无法准确表述遇到的问题时,一种笼统的陈述罢了。
duckduckgo会用你的查询语句到各个主流社交平台去搜索。但是它太看重社交平台了,因此得出的结果未免偏颇而狭隘。另外收录的内容过少,也是一个问题。如果搜索的内容不是错误描述或API之类,搜索出来的结果恐怕就如bing一样。
百度是用来搜索广告的。

只有Google,是现阶段唯一得我心思的搜索引擎。

说回来,搜索引擎对于程序员,就像刀对于厨师。如果没有一把趁手的刀,厨艺难免会打上折扣,厨师本人亦会不甚愉快。

Google之于我,已经不是一个普通的搜索引擎那么简单,而是彻底融入我的生活,成为我的工具箱的一部分。可以这么说,上不了Google,就像不能使用Vim来编辑文本,不能使用命令行来自动化繁琐的任务,让人感到沮丧而无奈。
所以说,GFW禁掉了Google,但是我还是想方设法绕过去,去访问google,访问一流的搜索引擎。
这不仅是我个人的想法,试下在v2ex下搜索“我要上谷歌”,看看许多同行为了正常地访问一个搜索引擎所付出的种种努力。有时候不禁这么想,禁掉Google,就是让程序员的生活质量大打折扣。

我要上谷歌。我还是会绞尽脑汁发掘一条通往Google的道路。即使荒草遍地,我还是会走出一条路。

三种阅读方式

不算完成scala作业和填find的terminal UI这个挖了一段时间的,好像有一段时间没有写代码呢……最近真是提不起干劲啊,也许得等天晴一些之后才重新回到热情饱满的状态。

为了打发时间,上来闲聊下吧。既然最近没有写代码,那也聊不了多少编程相关的话题。嗯,就聊点闲散的东西,聊聊阅读吧。

我觉得,阅读可以分为三种方式,翻页式、流式和超文本式。

  1. 翻页式:即阅读文本书时的方式,一页页翻下去,没翻到下一页之前就不知道后面发生的东西。
  2. 流式:即阅读PDF文档时的方式,一页页滚下去,看着下一页一点点加载上来。
  3. 超文本式:即阅读网页的方式,从一个地方跳转到另一个地方,然后又跳回来,也许就再也跳不回来了。

这三种方式带来的感觉真是不一样的。换句话说,各有特点吧。

翻页式是最自然的,尤其是当页脚卷起,触及肌肤之时,感觉整个人都很有状态呢:)。当然啦,毕竟这是在阅读实体书,而实体书带来的质感,是电子书所不能披及的。有些电子阅读应用试图复制这一体验,但在我看来,不过是东施效颦罢了。毕竟即使是“触碰”,发生在纸上和发生在触摸屏上,感觉是大相径庭的。这种拙劣的效仿,只会加深这种不真实感。

流式是阅读PDF的形式。虽然也可以把PDF浏览器做成翻页的样子,但是不比txt文件文件,PDF的大小不能轻松转换。如果硬要做成一页页的样子,假设用户需要放大缩小当页的内容,计算排版的任务可就有得忙啦,说不定就卡死了……流式阅读有个特点,其内容是逐渐拉上来的,而且页与页间的间隔不明显,没有翻页式那种内容被切割成一块块的感觉。不过,也因此迷失了进度,不容易知道自己到底阅读了多少。

超文本式是一种奇妙的阅读体验。你所面对的,不是一本有穷的书,而是无穷的知识网络。就像是跑到一个公园里,你追踪着各式各样的风景不断前行,突然又路过了之前的某个分叉路口。它跟之前的阅读方式都有很大的不同,以至于有人甚至不认为这算是一种阅读方式。不过事实上,恐怕跟前面两种方式相比,它跟现实生活中获取知识的方式是最为契合的。毕竟生活的知识,就是散落在世界这一广袤天地的各处。不过,要是想获得某种连续的,有趣的体验,比如去阅读一本小说,这种方式总会打断读者的遐想。所以,超文本式适合去求知,而想从想像的世界中获取乐趣,还是选择传统的阅读方式吧。

虽说,阅读这件事,主要还是看阅读的材料吧。但是,阅读的方式,还是大大影响了阅读的体验呀。

dash/zeal添加新文档

dash介绍

dash是一个收费的文档浏览应用。什么是文档浏览应用呢?就是将文档的HTML源格式打包成docset,该docset对文档中的条目加了索引,然后用户就可以在应用内查询相关的条目内容。当然dash的功能不仅止于浏览文档,如果感兴趣的话可以去官网接受下安利。

如果你之前没有用过类似的文档应用,建议现在就用起来。它们可以显著地提高文档查找的效率,而查文档又是日常编程中常做的事情之一。

如果你不是OS X用户,可以试下zeal,一个开源的dash实现(支持Windows和Linux)。如果觉得dash的价格太贵,也可以试下devdocs,我会在下一篇文章中讲讲devdocs相关的内容。

如何生成dash docset

docset包含三部分:源文档的HTML文件、跟文档展示相关的静态资源、用于索引的sqlite表。
生成docset,简单来说就是处理源文档的HTML文件,然后提取出条目,并写入sqlite。具体的生成方式见https://kapeli.com/docsets 。如果需要生成的文档是由godoc之类的生成器生成的,由于它们遵循同样的格式,可以直接用别人写好的工具生成。否则的话,需要自己解析源文档的HTML文件。下面我将以openresty项目为例,阐述下dash docset的生成步骤。

创建一个文档需要以下几步(假设需要生成的文档名为docset_name):

  • 创建<docset_name>.docset/Contents/Resources/Documents/文件夹。
  • 复制文档的HTML源文件到上面的文件夹中。 注意其中包括HTML中引用的css/image等外部资源,如果需要的话,修改HTML中对这些资源的引用路径。
resources = set()
    rewritten_head = '<title>%s</title>\n' % metadata.name
    for css in soup.findAll('link', rel='stylesheet'):
        link = css['href'].rpartition('/')[-1]
        resources.add(Resource(filename=link, url=css['href']))
        new_css = soup.new_tag('link')
        new_css['rel'] = 'stylesheet'
        new_css['href'] = link
        rewritten_head += str(new_css)
  • <docset_name>.docset/Contents文件夹下创建文件Info.plist。官方教程建议直接从模板上改。
  • <docset_name>.docset/Contents/Resources/docSet.dsidx文件中创建SQLites的表和索引。创建表的SQL语法为CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT);,创建索引的SQL语法为CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);。这里name将会是条目的名字。而type是条目的类型。path是HTML源文件中所对应的路径。举个例子,C.docset中的fopen文档,它在这个表里面的表示方式是这样的:
name type path
fopen Function ./c/io/fopen.html

dash所支持的类型type取值见 https://kapeli.com/docsets#supportedentrytypes

def write_sql_schema(fn='OpenResty.docset/Contents/Resources/docSet.dsidx'):
    db = sqlite3.connect(fn)
    cur = db.cursor()
    try:
        cur.execute('DROP TABLE searchIndex;')
    except Exception:
        pass
    cur.execute('CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT);')
    cur.execute('CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);')
    db.commit()
    db.close()
  • 最困难的一步来了,你需要写一个脚本,从HTML源文件中提取内容,并插入到上一步所创建的表。
entries = []
    readme = soup.find(id='readme')
    base_path = '%s.html' % metadata.name

    def handle_each_section(section_header, section_type, entry_header, namespace):
        for tag in section_header.next_siblings:
            # not all siblings are tags
            if not hasattr(tag, 'name'):
                continue
            if tag.name == section_header.name:
                break
            if tag.name == entry_header:
                api_name = next(tag.stripped_strings)
                tag_anchor = next(tag.children)
                entry_path = base_path + tag_anchor['href']
                entries.append(Entry(
                    name=api_name, type=section_type, path=entry_path))
                # insert an anchor to support table of contents
                # and more ...

    if metadata.name == 'lua-resty-websocket':
        for section in metadata.sections:
            section_path = section.replace('.', '')
            entries.append(Entry(
                name=section, type='Class', path=base_path + '#' + section_path))
            section_header = soup.find(
                id=('user-content-' + section_path)).parent
            handle_each_section(section_header, 'Method', 'h4', section)
    else:
        for section in metadata.sections:
            section_type = get_type(section)
            section_header = soup.find(id=('user-content-' + section)).parent
            # all entries' header is one level lower than section's header
            entry_header = 'h' + str(int(section_header.name[1]) + 1)
            handle_each_section(
                section_header, section_type, entry_header, metadata.name)

    # remove user-content- to enable fragment href
    start_from = len('user-content-')
    for anchor in soup.findAll('a'):
        if 'id' in anchor.attrs:
            anchor['id'] = anchor['id'][start_from:]

以下均为可选步骤:

  • 给每页内容添加条目划分。dash会从HTML源文件中提取格式为<a name="//apple_ref/cpp/Entry Type/Entry Name" class="dashAnchor"></a>的标签,作为显示在左下角的条目划分的依据。Entry Type表示该条目的type,如前面提到的Function。而Entry Name里的内容将显示在条目划分中。 注意Entry Name的值需要经过URL编码处理 。然后往前面的Info.plist添加这两行:
<key>DashDocSetFamily</key>
<string>dashtoc</string>

Info.plist的修改需要在重新加载docset后才能生效。
另外,zeal也有相似的条目划分功能,不过它的实现跟dash不同。zeal会使用当前页面的路径,查询以该路径开头的其他条目。不过值得注意的是,zeal这一实现存在个问题:如果条目的路径以./开头,zeal是查询不到的(因为“当前页面的路径”不包含./)。(做OpenResty的docset时,我一直搞不懂为什么条目划分没有生效,直到阅读了zeal的实现源码才知道是这么一回事)

# insert an anchor to support table of contents
anchor = soup.new_tag('a')
anchor['name'] = '//apple_ref/cpp/%s/%s' % (section_type, quote(api_name))
anchor['class'] = 'dashAnchor'
tag_anchor.insert_before(anchor)
  • 添加图标到<docset_name>.docset/icon.png。图标规格最好是32X32,你也可以准备两个图标文件,一个是icon.png(16x16),另一个是[email protected](32x32)。后者将会用在Retina屏幕上。

  • 添加文档重定向支持。为了让用户能够打开在线文档,你可以在docset里添加文档重定向的功能。有两种办法:

    1. Info.plist添加
    <key>DashDocSetFallbackURL</key>
    <string>$baseURL</string>
    
    其中的`$baseURL`为在线文档的入口地址。
    
    1. 在每个HTML文件的<html>旁添加源地址的注释,像这样:<html><!-- Online page at https://docs.python.org/3/library/intro.html -->
  • 启用JavaScript。Javascript默认是禁用的,要想启用,要在Info.plist中添加<key>isJavaScriptEnabled</key><true/>

  • 添加docset的主页。当你点击某个docset时,它会尝试显示一个主页。你需要在Info.plist添加主页的路径:

<key>dashIndexFilePath</key>
<string>$PATH</string>

其中$PATH为主页相对于<docset_name>.docset/Contents/Resources/Documents/的路径,如api.jquery.com/index.html

生成OpenResty docset的完整代码见github

git merge是怎样判定冲突的?

在解决git merge的冲突时,有时我总忍不住吐槽git实在太不智能了,明明仅仅是往代码里面插入几行,没想到合并就失败了,只能手工去一个个确认。真不知道git的合并冲突是怎么判定的。

在一次解决了涉及几十个文件的合并冲突后(整整花了我一个晚上和一个早上的时间!),我终于下定决心,去看一下git merge代码里面冲突判定的具体实现。正所谓冤有头债有主,至少下次遇到同样的问题时就可以知道自己栽在谁的手里了。于是就有了这样一篇文章,讲讲git merge内部的冲突判定机制。

recursive three-way merge和ancestor

git的源码
先用merge作关键字搜索,看看涉及的相关代码。
找了一段时间,找到了git merge的时候,比较待合并文件的函数入口:ll_merge。另外还有一份文档,它也指出ll_merge正是合并实现的入口。

从函数签名可以看到,mmfile_t应该就代表了待合并的文件。有趣的是,这里待合并的文件并不是两份,而是三份。

int ll_merge(mmbuffer_t *result_buf,
         const char *path,
         mmfile_t *ancestor, const char *ancestor_label,
         mmfile_t *ours, const char *our_label,
         mmfile_t *theirs, const char *their_label,
         const struct ll_merge_options *opts)

看过git help merge的读者应该知道,ours表示当前分支,theirs表示待合并分支。看得出来,这个函数就是把某个文件在不同分支上的版本合并在一起。那么ancestor又是位于哪个分支呢?倒过来从调用方开始阅读代码,可以看出大体的流程是这样的,git merge会找出三个commit,然后对每个待合并的文件调用ll_merge,生成最终的合并结果。按注释的说法,ancestor是后面两个commit(ourstheirs)的公共祖先(ancestor)。另外前面提到的文档也说明,git合并的时候使用的是recursive three-way merge

three-way merge

关于recursive three-way merge, wikipedia上有个相关的介绍。就是在合并的时候,将ours,theirs和ancestor三个版本的文件进行比较,获取ours和ancestor的diff,以及theirs和ancestor的diff,这样做能够发现两个不同的分支到底做了哪些改动。毕竟后面git需要判定冲突的内容,如果没有原初版本的信息,只是简单地比较两个文件,是做不到的。

鉴于我的目标是发掘git判定冲突的机制,所以没有去看git里面查找ancestor的实现。不过只需肉眼在图形化界面里瞅上一眼,就可以找到ancestor commit。(比如在gitlab的network界面中,回溯两个分支的commit线,一直到岔路口)

有一点需要注意的是,revert一个commit不会改变它的ancestor。所谓的revert,只是在当前commit的上面添加了新的undo commit,并没有改变“岔路口”的位置。不要想当然地认为,revert之后ancestor就变成上一个commit的ancestor了。尤其是在revert merge commit的时候,总是容易忘掉这个事实。假如你revert了一个merge commit,在重新merge的时候,git所参照的ancestor将不是merge之前的ancestor,而是revert之后的ancestor。于是就掉到坑里去了。建议所有读者都看一下git官方对于revert merge commit潜在后果的说法:https://github.com/git/git/blob/master/Documentation/howto/revert-a-faulty-merge.txt
结论是,如果一个merge commit引入的bug容易修复,请不要轻易revert一个merge commit。

剖析xdiff

ll_merge往下追,可以看到后面出了一条旁路:ll_binary_merge。这个函数专门处理bin类型文件的合并。它的实现简单粗暴,如果你没有指定合并策略(theris或ours),直接报Cannot merge binary files错误。看来在git看来,二进制文件并没有diff的价值。

主路径从ll_xdl_mergexdl_merge,进到一个叫xdiff的库中。终于找到git merge的具体实现了。

平心而论,xdiff的代码风格十分糟糕,不仅注释太少,而且结构体成员变量居然使用类似i1、i2这样的命名,看得我头昏脑胀、心烦意燥。

吐槽结束,先讲下xdl_merge的流程。xdl_merge做了下面四件事:

  1. xdl_do_diff完成two-way diff(ours和ancestor,theirs和ancestor),生成修改记录,存储到xdfenv_t中。
  2. xdl_change_compact压缩相邻的修改记录,再用xdl_build_script建立xdchange_t链表,记录双方修改。xdchange_t主要包括了修改的起始行号和修改范围。
  3. 这时候分三种情况,其中两种是只有一方有修改(只有ours或theirs一条链表),直接退出。最后一种是双方都有修改,需要合并修改记录。由于修改记录是按行号有序排列的,所以直接合并两个链表。修改记录如果没有重叠部分,按先后顺序标记为我方修改/他方修改。如果发生了重叠,就表示发生了冲突。之后会重新过一遍两个待合并链表,对于那些标记为冲突的部分,比较它们是否相等的,如果是,标记为双方修改。
  4. xdl_fill_merge_buffer输出合并结果。如果有冲突,调用fill_conflict_hunk输出冲突情况。如果没有冲突(标记为我方修改/他方修改/双方修改),则合并ancestor的原内容和修改记录,按标记的类型取修改后的内容,并输出。

输出冲突情况的代码位于fill_conflict_hunk中。它的实现很简单,毕竟此时我们已经有了双方修改的内容,现在只需要同时输出冲突内容,供用户取舍。(这便是那次花了一个晚上和一个早上改掉的冲突的源头,凶手就是你,哼)。

输出格式恐怕大家都很熟悉。该函数会先打印若干个<,个数由DEFAULT_CONFLICT_MARKER_SIZE决定,也即是7个。然后是ours分支名。接着输出我方的修改,然后输出若干个=。最后是他方的修改,以及若干个>。这个就是折磨人的合并冲突了:

<<<<<<< HEAD
3
=======
2
>>>>>>> branch1

总结

git merge的冲突判定机制如下:先寻找两个commit的公共祖先,比较同一个文件分别在ours和theirs下对于公共祖先的差异,然后合并这两组差异。如果双方同时修改了一处地方且修改内容不同,就判定为合并冲突,依次输出双方修改的内容。

Fate Stay Night Fate线动画剧情

最近一个月来,我追完了Fate Stay Night fate线动画全部24集,外加各种特典,另外重看了部分片段。(吾王**!)真是非常棒的说!等UBW线完结后,我会第一时间去补UBW动画的!

看完感觉:超爱Saber!(我也希望会有一个像Saber那样坚毅、勇敢又善良的女朋友)不过,最后士郎和Saber没有能够在一起,“虽然看上去那么近,但是伸手却抓不到”,看到这里我整个人都不舒服了。完全一种淡淡的忧伤的感觉,即使最终打败了Boss,有情人终难免永别……本来希望编剧能够有个神来之笔,用魔法让Saber复活。不过应死之人终究会走入死亡,Saber还是回到她来的时空,安详地魂归Avalon。

另外,尽管动画中没有说明,根据游戏的设定,伊莉雅也只剩下一年的生命了……虽然结尾的时候她还是那般精神奕奕,但是作为人造人,短暂的生命就像诅咒一样,无形地压在命运上。多可怜啊,这么一个可爱的萝莉。(其实人家不是萝莉!只是8岁之后就停止生长了。人家是切嗣的女儿,士郎的姐姐,年纪在一帮高中生中是最大的。嗯,ACG当中真是无限可能。)

圣杯战争是,被选中的魔法师们为了争夺可以实现愿望的圣杯而进行的互相争斗。同样是讲述不真实的生存游戏,圣杯战争的概念就比《饥饿游戏》好的多。虽然《饥饿游戏》扯上了反压迫的大道理,不过比起Fate Stay Night中的故事,未免显得太过平平。有趣的是,动画的结尾不是好人从坏人手中夺取圣杯,然后世界重新恢复爱与和平。那样就成了中二病动画……

事实上,圣杯只是诱饵,可怜的魔法师们,只是一场仪式所不能缺少的部分。当圣杯出现在人们的眼前,人们才知道,原来一直苦苦追求的东西,其实是“恶”之化身。于是,切嗣也好,士郎也好,最后都选择破坏圣杯。不同的是,切嗣需要对抗的,只是之前对于圣杯的渴望;而士郎一旦选择破坏圣杯,就是选择结束圣杯战争,也即结束Saber的使命(寿命)。最后,Saber说,“我希望听到你的声音”,士郎下定了决心,命令Saber发动誓约胜利之剑,向圣杯划出一击……

让我们回过来看看圣杯御三家,爱因贝兹、间桐和远坂。为了圣杯,这三家每60年就会进行杀戮和被杀戮,结果是为了追求一种灾难,真是家族的不幸。如果看了HF线的剧情(还有Fate Zero的),就难免为他们三家的不幸而感到惋惜。

最后看看动画中第二对未能成眷属的有情人,caster和葛木。在Fate线中这对戏份不多,很多事情都没有交代,给人一种坏蛋情侣最终双双死去的感觉。不过caster临死前那句,“我的愿望,截止到现在,一直都是实现的”,-- 跟心爱的人在一起,就是这么难么 -- 真是让人不禁心里泪落。其实Caster追求圣杯,也许是追求圣杯期许的第二生命,否则就不能活下来跟心爱的人在一起了。可惜人算不如天算,幕后Boss早已安排了一切,原以为大事即将完成,结果半路杀出强敌,反而丢了卿卿性命。

button不只是button

今天写前端页面的时候发现了一个有趣的事情,或者说bug吧。点击一个按钮,会导致页面跳转到404。查了下浏览器请求,发现是一个POST请求导致的。这个页面包含了一个form,确实是想让它POST数据给后台。不过按照设定,应该是通过AJAX实现的,而不是直接重新加载页面。

看来不知道是什么触发了表单的submit事件。仔细阅读下相关的代码,这个按钮被按下时,会去检查表单是否正确,如果正确,则通过AJAX去进行POST。注意,这个按钮只是普通的<button>,并不是<input type="submit">。然而在页面跳转的同时,检查表单正确性部分的代码也不能工作了。

因为浏览器在POST之后会重新加载内容,不会保留之前的log,这给调试带来了麻烦。花了我不少时间,终于把bug揪出来了。原来是某一个分支中,event.preventDefault()被绕过了,导致原来的事件没有拦住。一开始发现这一点时,我是感到很奇怪的,原来的事件不只是click么?怎么会是submit呢?

查了下,原来button元素默认是type="submit",也即在form中,点击button自然会触发submit事件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <form action="/test" method="post">
    <button>don't submit</button>
  </form>
</body>
</html>

即使这个button已经标明是“don't submit”了,呵呵。
解决之道很简单,就是指定type="button",这样它就是人畜无害、天真无邪的好button了。

如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <form action="/test" method="post">
    <button>submit</button>
    <button type="button">don't submit</button>
  </form>
  <button>just click it</button>
</body>
</html>

如果这个buttonform之外,那么就不会触发submit事件。然而在html5标准中,你可以通过设置form="#form_id"属性来给button指定一个非直系祖先的form

这篇debug小文本来打算发到SegmentFault上的,不过考虑到它实在太短,干货还不够多,所以就放到这里了。

第一次身处黑客入侵的事故现场

昨天下午我正在图书馆刷书复习,突然接到同学的电话。同学是学校某个机构的助理。他告诉我他办公室有台Linux服务器出了问题,总是把带宽占满。情况紧急,办公室老师让他赶紧联系认识的同学,看看能不能帮忙处理下。
本来我是拒绝的……因为我虽然日常都使用Linux,但是从来没弄过服务器。把Linux当做主力桌面环境和把Linux当做服务器伺候,两者是有区别的。所以我让同学再找别人。过了一段时间,他又重新打了电话过来,说是找不到别的人了。于是我只有硬着头皮上了>_<
跟他汇合之后,搭上电梯前往服务器所在的机房。好家伙,第一次进机房,感觉来到一个神殿,四处耸立的机柜中供奉着威严的神灵。四周响起低沉的嗡嗡声,就像看不见的怪虫在齐鸣。
当然我不是来参观机房的。已经有一个网管在现场了。看到还有别人在,我内心顿时轻松了些许,看来还至于硬要我来……于是连接显示器,打开有问题的服务器。登录进去后,迎接我们的是一个像爱丽丝漫游的仙境一样怪异的世界:窗口古怪地变形着,你甚至找不到打开软件的菜单,一切无比地陌生,如同突然陷入达利的《记忆的永恒》

记忆的永恒

这里先打断一下,这个服务器是有桌面环境的……还好它有桌面环境,不然待会我只能通过w3m来上网了(我手上什么都没带)。震惊了一下子后,我们换了另外一种打开方式,退回到登录界面。刚好发现下面有一个设置,可以选择Gnome窗口登入。于是我们来到了熟悉的Gnome世界……

情况紧急,废话不多说,网管说了下他知道的大概的情况,就是服务器会吞掉所有的带宽限额,导致正常的服务无法运行。输入ifconfig命令,我们当时看到发送的数据已经达到了令人咋舌的地步了……

令人咋舌

我们折腾了一番,终于看到了嫌疑犯。使用top命令,看到当前运行的进程中,有一个很可疑的进程,叫做sbin。为什么觉得它很可疑呢?原因有三:

  1. 这个进程占有大量的内存和CPU,如果不是第一,那也是前三。
  2. 这个进程不能用ps显示出来,只能通过top才看得到。
  3. sbin不应该是文件夹的名字么?什么时候变成可执行文件了……这也太明显了!

毫无疑问,服务器被入侵了。sbin作为嫌疑犯,自然遭到了我们的“亲切拜访”。which sbin定位sbin的位置,好,找到了:/var/cache/sbin

嫌疑犯

cd过去,呃,我对这个目录一无所知……还好能够求助于万能的Google。原来var文件夹存储的是临时的变量,不过下面的cache文件夹作用不大,删了也没多大关系。这时我们又注意到,除了sbin之外,我们还看到其他知名可执行文件,比如manps等等。这下终于明白为什么ps没法找到sbin进程了。查一下PATH变量,果不其然。ls -al一下(当然这里开始就使用绝对路径来调用命令了,还测试了几个知名命令,确认没有更多的冒牌货潜伏其中),看看具体信息,这几个文件都是在几个月前的两分钟内放进来的。

接下来就差证据了。查了一下,发现netstat命令有一个-p参数可以显示某个进程的网络连接情况。于是netstat -p | grep sbin,我们看到了令人震惊的真相……

真相

这个sbin进程建立了大量的pptp协议的链接,就是它导致了服务器的流量暴增的情况。可惜我当时没有拍照,不然就拿出来让大家分析好了。

既然已经找到了罪魁祸首,那么把它杀掉,服务器的问题就解决了……不过这个只是解决了表面问题,我们还是不得而知骇客是怎么进来的,不能排除贼惦记的可能。不过因为网管要下班了,催促我们离开,所以准备明天再深究下去。然后我就继续刷我的书去了。直到睡前才码下这篇文章,总结一下。

最后问诸位一个问题:接下来我应该怎么调查呢?

已知疑点:

  1. 这个进程启动的是pptp点对点协议的服务
  2. 黑客把东西都放在/var/cache/文件夹下,时间是几个月前,但是问题是最近才出现的。
  3. 同时我们发现,整个硬盘被撑爆了,不能通过yum来下载任何东西,甚至连用vi编辑文件都不能(无法产生缓冲区)。

不知诸位有何推论?下一步该前往何方?

2014/7/4 更新

今天早上又去看了下……原来是黑客放了一系列脚本到/var/cache下。其中有一个install脚本,一旦执行它,就会添加一个用户到etc/passwdetc/sudoers中,这样黑客就能通过这个用户登录并获得管理员权限了。而且这个脚本还会调用一连串其他脚本,最终在后台执行sbin这个程序。这样一来,把这个用户以及它的相关文件删掉就好了。猜测黑客也没有获得别的账号的密码,否则他就不需要自己弄一个账号出来。

至于为什么硬盘被撑爆了……这个跟黑客无关,是因为服务器长期没有打理,东西堆太多(而且本来硬盘也不大)。我们查了下几个占用空间特别大的文件夹,里面的文件都是黑客袭击之前就有的,而且都是正常的文件。

这次黑客入侵的事件大概可以告一段落了。虽然还是不知道黑客是怎么入侵到var/cache文件夹的,不过好在这个黑客并不高明,留下了太多的证据,让我们毫不费力地找到了真凶。

跟我一起写shell补全脚本(Zsh篇)

绝大部分日常使用Linux和OS X的程序员都会选择zsh作为自己的shell环境,毕竟对比于bash,zsh的便利性/可玩性要胜出很多,同时它又能兼容bash大多数的语法。不过相对而言,zsh补全脚本要比bash补全脚本要难写。zsh提供了非常多的补全的API,而且这些API功能有不少重叠的地方,掌握起来并不容易。不像bash,你只需记住三个API(compgencompletecompopt)就能实现整个补全脚本。

这篇的任务跟上一篇的一样,需要实现一个针对pandoc的补全脚本,囊括下面三个目标:

  1. 支持主选项(General options)
  2. 支持子选项(Reader options/General writer options)
  3. 支持给选项提供参数值来源

支持主选项

还是跟上一篇一样,先解释一个实现第一个目标的程序,带各位入门:

#compdef pandoc

_arguments \
    {-f,-r}'[-f FORMAT, -r FORMAT, Specify input format]' \
    {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]' \
    {-o,--output}'[-o FILE, --output=FILE, Write output to FILE instead of stdout]' \
    {-h,--help}'[Show usage message]' \
    {-v,--version}'[Print version]' \
    '*:files:_files'

就像bash的complete,zsh也有一个相对的表示补全的API,就是compdef。zsh补全脚本以#compdef tools开头,表示该文件是针对tools的补全脚本。当然你也可以像bash一样,直接compdef _function tools来指定tools的补全函数。

zsh补全API的第一梯队是_alternative_arguments_describe_gnu_generic_regex_arguments。它们直接提供补全的来源。这些API的概述见https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org#main-utility-functions-for-overall-completion。由于`_describe`能做的`_arguments`也能做,`_gnu_generic`是为GNU拓展的命令参数准备的,`_regex_arguments`就是正则匹配版的`_arguments`,所以只要记住`_arguments`和`_alternative`就够用了。

_arguments接受一连串的选项字符串,每个字符串代表一个选项。另外你还可以通过一些选项指定补全上的细节。举-s为例:假设你的工具支持-a -b两个选项,也支持-ab的方式来同时指定两个选项。如果没给_arguments提供-s的选项,那么zsh是不会补全出-ab,因为并不存在选项-ab。而提供了-s后,_arguments才允许你在已经输入-a的情况下,补全出-ab

选项字符串的格式是这样的:-x[description]:message:action。你也可以写做{-x,-y}[description]:message:action形式,表示-x-y是等价的写法。

  1. -x是选项的名字
  2. [description]是该选项的描述,可选
  3. message这一项我也不知道是什么意义……不过它是可选的,除非你需要指定action
  4. action用于生成复杂的补全。在这里你可以使用许多补全语法。一个常见的例子是使用辅助函数,比如_files表示补全当前路径下的文件名。详见:
    1. https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org#actions
    2. https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org#functions-for-completing-specific-types-of-objects

最后一行'*:files:_files'表示,如果找不到匹配的候选词,就补全文件名。
到目前为止,实现第一阶段目标的脚本所需的知识点已经讲解完毕。

_arguments有一个限制,它要求选项的名字符合某些特殊格式,比如以-+=等字符开头(所以才叫_arguments嘛)。如果你的工具接受addremove之类的子命令,就需要用到_alternative

_alternative支持的选项字符串格式跟_arguments很像,比如

_arguments \
    {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]'

等价于

_alternative \
    'writer:writer options:((-t\:"-t FORMAT, -w FORMAT, Specify output format" -w\:"-t FORMAT, -w FORMAT, Specify output format"))'

支持子选项

所谓的支持子选项,就是在某些选项存在的情况下,增加多一些选项。所以,我们所要做的,就是检查当前输入的命令行参数中是否存在某些参数,如果存在,增加新的选项。这一步可以分解成两个步骤,第一个是检查某些参数是否存在,第二个是增加新的选项。

之前写bash补全脚本的时候,是通过遍历某个存储有当前输入的常量数组,来检查某些参数是否存在。在网上搜索一番后,我发现zsh也有同样的常量数组,就叫做words,正好是bash那个的小写哈。那么接下来就是zsh的语法知识了:

if [[ ${words[(i)-f]} -le ${#words} ]] || [[ ${words[(i)-r]} -le ${#words} ]]
then
    # 修改补全候选列表
fi

if [[ ${words[(i)-t]} -le ${#words} ]] || [[ ${words[(i)-w]} -le ${#words} ]]
then
    # 修改补全候选列表
fi

这里用到一点zsh特有的下标语法,相当于index()

那么下面是第二步,该怎么修改补全候选列表呢?如果直接用_arguments指定新的补全列表,会覆盖掉前面指定的补全列表。当然也可以把前面的补全列表复制一份,并添加新的选项,用它覆盖掉原来的补全列表。不过这么一来代码就不好看了。

想来zsh应该提供了对应的API的。果不其然,有一个_values可以用来干这事。_values功能跟_arguments差不多,而且它接受的选项列表是添加到原有的选项列表中的,而不是覆盖。所以最后的代码是这样的:

if [[ ${words[(i)-f]} -le ${#words} ]] || [[ ${words[(i)-r]} -le ${#words} ]]
then
    _values 'reader options' \
        '-R[Parse untranslatable HTML codes and LaTeX as raw]' \
        '-S[Produce typographically correct output]' \
        '--filter[Specify an executable to be used as a filter]' \
        '-p[Preserve tabs instead of converting them to spaces]'
fi

if [[ ${words[(i)-t]} -le ${#words} ]] || [[ ${words[(i)-w]} -le ${#words} ]]
then
    _values 'writer options' \
        '-s[Produce output with an appropriate  header  and  footer]' \
        '--template[Use FILE as a custom template for the generated document]' \
        '--toc[Include an automatically generated table of contents]'
fi

支持给选项提供参数值来源

最后一步是给-f-r这两个选项提供读操作支持的FORMAT参数,给-t-w这两个选项提供写操作支持的FORMAT参数。

在Bash篇的实现中,我们检查上一个词的值,如果它是-f-r,那么对当前词补全读操作的FORMAT参数。对写操作的选项也同理。
在zsh中,我们可以用一个特殊的Action:->VALUE来实现。

->VALUE这样的Action会把$state变量设置成VALUE,接下来靠一个case语句块就能根据当前陷入的状态进行对应的参数补全。

那么该如何补全FORMAT参数列表呢?这里可以用上_multi_parts
_multi_parts第一个参数是分隔符,之后接受一组候选词或一个候选词数组作为候选词列表。例如_multi_parts , a,b,c,就会生成a b c这个补全候选列表。

这里的FORMAT变量直接使用上一章的$READ_FORMAT$WRITE_FORMAT
我试了一下,如果把FORMAT变量当做字符串传递过去的话,其间的空格会被转义,导致无法分隔开来,于是就把它们改写成数组的形式。

另外,由于补全FORMAT参数时,不再需要补全选项了。所以把补全FORMAT参数的部分提到补全子选项的前面,并在补全后直接退出程序的执行。

最终完成的代码如下:

#compdef pandoc

local READ_FORMAT WRITE_FORMAT
READ_FORMAT='(native json markdown markdown_strict markdown_phpextra 
markdown_github textile rst html docbook opml mediawiki haddock latex)'
WRITE_FORMAT='(native json plain markdown markdown_strict 
markdown_phpextra markdown_github rst html html5 latex beamer context 
man mediawiki textileorg textinfo opml docbook opendocument odt docx 
rtf epub epub3 fb2 asciidoc slidy slideous dzslides revealjs s5)'

_arguments \
    {-f,-r}'[-f FORMAT, -r FORMAT, Specify input format]: :->reader' \
    {-t,-w}'[-t FORMAT, -w FORMAT, Specify output format]: :->writer' \
    {-o,--output}'[-o FILE, --output=FILE, Write output to FILE instead of stdout]' \
    {-h,--help}'[Show usage message]' \
    {-v,--version}'[Print version]' \
    '*:files:_files'

case "$state" in
    reader )
        _multi_parts ' ' $READ_FORMAT && return 0
        ;;
    writer )
        _multi_parts ' ' $WRITE_FORMAT && return 0
esac

if [[ ${words[(i)-f]} -le ${#words} ]] || [[ ${words[(i)-r]} -le ${#words} ]]
then
    _values 'reader options' \
        '-R[Parse untranslatable HTML codes and LaTeX as raw]' \
        '-S[Produce typographically correct output]' \
        '--filter[Specify an executable to be used as a filter]' \
        '-p[Preserve tabs instead of converting them to spaces]'
fi

if [[ ${words[(i)-t]} -le ${#words} ]] || [[ ${words[(i)-w]} -le ${#words} ]]
then
    _values 'writer options' \
        '-s[Produce output with an appropriate  header  and  footer]' \
        '--template[Use FILE as a custom template for the generated document]' \
        '--toc[Include an automatically generated table of contents]'
fi

后话

由于zsh的补全功能实在强大,而这篇文章只是简略地讲讲如何写出一个zsh补全脚本,有许多zsh的补全机制都没能提到。所以补充一些写zsh补全脚本的资料,如果对这方面有兴趣可以继续跳坑:

  1. zsh-completions项目上的教程。这是我见过的最详尽的zsh补全脚本教程。
  2. 官方文档
  3. /usr/share/zsh/functions/Completion 也许你能从相似的命令的补全脚本中汲取灵感。

顺便一提,在查找资料的时候发现有人写了一个完整的pandoc的zsh补全脚本,感兴趣的话可以看一下:
https://github.com/srijanshetty/zsh-pandoc-completion/blob/master/_pandoc

奇怪的Go时间格式字符串

因为需要在Go语言里格式化输出时间,所以看了下对应的文档。看了之后我整个人都囧掉了,感觉就像是在唐诗精选里面读到一首打油诗。
Go里面格式化时间的方式实在太奇怪了……一般情况下,我们都是使用类似于%Y-%m-%D或者yyyy-mm-dd的格式字符串来格式化输出时间。Go也不例外。只是Go里面不是用y啊m之类抽象的符号,而是直接使用数字和英语单词……

先记住这个时间点:2006-01-02T15:04:05Z07:00,这是个星期一。
如果你把上面的时间点作为格式化字符串放到Time.Parse里面,它就会以“年-月-日T时:分:秒”的格式输出时间。对的,你没看错,上面那串时间就是合法的、正规的、受到推荐的Go里面的时间格式化字符串!Go通过琅琅上口(打油诗般)的字词,解决了其它语言里面格式符号难懂、难记、难写的困难,值得我们颁发年度最佳设计奖!

一开始我以为Go里面这么奇葩的设计,是为了纪念2006年1月2日下午3点4分5秒,那个改变世界的时刻。虽然我托着脸颊,沉思了一分钟,依然想不起那一天发生了什么事。也许是Go设计出来的日子?不过Go的年纪还不致于这么大……于是搜索一下,在这个帖子里面发现了那一天的真相

https://www.quora.com/Why-is-the-Golang-reference-time-Jan-2-2006-at-3-04pm-MST

原来是出于Explicit is better than implicit的考虑。
1表示月份,2表示天,3是那天下午,4是分,5是秒,6是年(2006),最后有一个7表示时区。我终于明白了这一切。据说这种表示方法连老奶奶也看得懂……顿时我就汗颜了,看来我不如老奶奶啊。也许老奶奶只是看得懂英文字符串,却不可能猜到它可以用来表示时间 😭
比起其它语言的yydd,Go这种时间格式字符串不用担心混淆MMmm哪个是月哪个是分,也不用担心表示年份该用yy还是YY。不过Go的问题是,3到底是小时呢,还是分钟呢?也许从1开始算起,我们就可以弄懂3到底是哪个时间单位。月、天、时,拇指、食指、中指,嗯,是第三个。不过是上午还是下午呢?本来想用24小时的,一不小心写成了12小时了…… 😞

不管怎么样,为了避免犯错,这里把Go的时间格式字符整理如下:

时间 Go中的表示法
2006
年(后两位) 06
1
月(带占位0) 01
2
日 (带占位0) 02
日(带占位空格) _2
星期几 Monday
时(24时) 15
时(12时) 3
时(12时,带占位0) 03
4
分(带占位0) 04
5
秒(带占位0) 05
时区 07:00

最后,我猜若干年以后,2006-01-02这一天会成为一个传奇,成为Go程序员口耳相传的mime,甚至会成为“公认”的Go诞生日。囧

小试Go

作为一个有志于开发服务端程序的人,如果没有尝试过Go,那实在是太可惜了。抱着这样的念头:),最近尝试下了Go。敲了些能找到的例子,不过目前暂时既没有用Go开发过side project,也没有给Go写的开源项目贡献代码(等开始结课后,时间变得更加充裕才继续)。刚刚统计了下,大致累积了三千多四千行。我觉得应该可以给Go一个阶段性判断了。

Go作为一门专为服务器端并发编程而生的系统编程语言,果然不负众望。goroutine和channel珠联璧合,编译速度更是出乎意料地快,非侵入接口实现了静态语言版Duck type。但是,由于没有泛型,有些设计真让人啼笑皆非。另外,其错误处理的机制也让我有点不习惯。总的来说,要想实现Go原初的目标 - 替代C - 还有很长一段路要走。

写啊写

Go作为Google的side project,也毫不意外地享受到GFW的“一视同仁”。golang主页如果不翻墙,是打不开的。关于golang的另一个资料来源,Google邮件组中的Go-nuts,也是得在墙外世界中。总之,如果你想学Go,那么翻墙吧。

现代编程语言往往提供all in one的工具链,方便用户进行编译/测试/打包/管理依赖等一系列工作。你可以尝试从Go China的国内镜像中获取,也可以使用包管理。包管理版本比较旧(我这里是Go 1.2,而最新是Go 1.4),但是安装方便,输完一行命令之后就会自动安装好了。鉴于本人比较懒,而且又不是狂热的Go fan,无需追求最新版,所以是使用包管理安装的。

开发工具直接使用vim + vim-go就好。Go开发非常轻便,无需一个沉重的IDE处理各种琐事。Keep it simple stupid!

困扰

Go的目标是Better C。所以它真的是挺simple的,而且跟C一样,也没有什么语法糖。嗯,这些也算是意料之中啦。只是Go的编译速度太快了,总是让人产生它是一门解释型语言的错觉。

跟其他的现代编程语言一样,Go也是把参数类型置于参数名字之后的。比如i int(v []int, cp func(int, int) bool) bool。一开始好不习惯,在写了一段时间后才适应了这种新规。即使这样,我还是觉得v int[]这种写法要比v []int更加合理。毕竟v是int类型的数组,而不是数组,int类型的。

Go还有一个坑,slice操作返回的是数组的一部分引用,而不是新的数组。我还是第一次碰到这么设计的,果不其然就掉到这个坑里。(我在微博吐槽过这件事)

Go原生支持UTF-8,只是支持的方式比较特别。由于UTF-8字符不是等长的,所以你不能通过按byte访问来正确获取UTF-8字符串中的各个字符。Go使用“码点”来解决这个问题。如果使用for...range循环来访问一个字符串,你就能访问到每一个UTF-8字符。如果你使用for i = ...; i < ...; i++,就是按byte访问。想要获得各个“码点”的顺序?先把字符串强转成符文数组[]rune,就可以以for i = ...; i < ...; i++访问各个“码点”了。Go这种做法,兼顾了UTF-8支持和底层操作的能力,其实挺好的。这样就完了吗?没有。len(str)返回的是字符串的byte数目。如果你想获取字符串的真正长度,需要先转换成[]rune,再取其大小。又一个令人困惑的地方。

接下来是时候总结下Go的优缺点了。鉴于前面吐槽了一堆,我还是先说优点吧。

优点

Go的编译速度实在太快了!以至于让人产生它是解释型语言的错觉。直接go run,一下子就能把程序运行起来。鉴于Go的工具链这么完备,且编译-运行周期这么短,同时不缺乏底层操作,完全可以作为大学的入门语言(如果不考虑就业的话),也可以作为拉小正太小萝莉入坑的语言(如果不考虑就业的话)。

另外,Go提供的非侵入接口也是一大创举(不知道是不是Go第一个提出来呢?)。这样子静态语言也能有Duck type。我觉得这种做法大有前景,将来应该会流行开来。

当然,提Go,绝对离不开其打天下的杀手锏 - goroutine和channel这对宝具。正是Go,让CSP并发模型名扬四海、妇孺皆知(大误)。
提CSP,就不能不提actor。这一对并发模式挺像的,只是前者关注于传递信息的通道(channel),后者关注于传递信息的对象(actor)。如果对这两种并发模式感兴趣,建议看下《Seven Concurrency Models in Seven Weeks》。鉴于这是在讲Go而不是讲并发,我一笔带过这部分好了:actor优点在于let it crash的容错机制。CSP优点在于更加关注于通信的渠道而非通信的对象,避免发送信息过多造成信息队列丢弃信息(在CSP中,这会导致阻塞),也避免发送信息的过程中由于对方节点崩溃而额外带来的错误处理。反正本人更青睐于CSP。

在Go中搞并发非常轻松。有多轻松?这的确很难量化,你也来试下,不就知道了?

缺点

Go的开发者们并不喜欢异常。即使异常已经成为主流编程语言不可或缺的一部分,连C++也开始拥抱异常了。但是C里面没有异常,作为better C,Go里面何须使用异常呢?所以Go还是沿用C的那一套。C是使用错误编码来报告出错的,但是一来调皮捣蛋的猴子们往往会无视返回值的处理,二来错误编码非常令人困惑,你得对着文档才能逐个处理好。所以作为better C,Go对此做了改进。Go返回Error对象报告错误,你可以子类化该对象来定义属于自己的错误编码。Error和结果一齐返回,这么一来猴子们就不会忘记使用if xxx; err != nil {来处理错误了。尽管如此,错误码永远比不上异常。你可以让高层次的代码处理异常,但是对于错误码,要想这么做,你得认真仔细地设计其调用体系。这样就对设计增加了额外的负担。你说用panicrecover?那又不能根据不同的异常情况进行不同的处理。

另外一个让人无法容忍的是,Go直到现在还是不支持泛型。之前看过一个Go社区的讨论,有人比较了C/C++/Java三者中实现泛型的方式。

...C选择麻烦程序员,C++选择麻烦编译,Java选择麻烦运行效率

所以到现在,Go还是没有决定好要麻烦谁,一直都没有推出泛型。在我认识的现代静态语言中,Go应该是唯一不支持泛型的。
所以有了这样的库函数设计:

Sort.Ints
Sort.Strings
Sort.Float64s
...

对,你没看错,Go针对每种基本类型实现了一次sort函数!
而且,因为没有泛型,Go里面不存在通用的max/min操作。由于Go里面也没有三元比较符,为了实现max,你需要这么写:

max typeX
if compare(a, b) > 0 {
    max = a
} else {
    max = b
}

值得一试

最后,虽然Go不能成为我心目中最喜欢的语言,但是Go还是值得一试的。我也会推荐别人尝试下Go。Go现已加入我的豪华套餐 >_< 。

关于Python Magic Method的若干脑洞

有一天闲着无聊的时候,脑子里突然冒出一个Magic Method的有趣用法,可以用__getattr__来实现Python版的method_missing
顺着这个脑洞想下去,我发现Python的Magic Method确实有很多妙用之处。故在此记下几种有趣(也可能有用的)Magic Method技巧,希望可以抛砖引玉,打开诸位读者的脑洞,想出更加奇妙的用法。

如果对Magic Method的了解仅仅停留在知道这个术语和若干个常用方法上(如__lt____str____len__),可以阅读下这份教程,看看Magic Method可以用来做些什么。

Python method_missing

先从最初的脑洞开始吧。曾几何时,Ruby社区的人总是夸耀Ruby的强大的元编程能力,其中method_missing更是不可或缺的特性。通过调用BaseObject上的method_missing,Ruby可以实现在调用不存在的属性时进行拦截,并动态生成对应的属性。

Ruby例子

# 来自于Ruby文档: http://ruby-doc.org/core-2.2.0/BasicObject.html#method-i-method_missing
class Roman
  def roman_to_int(str)
    # ...
  end
  def method_missing(methId)
    str = methId.id2name
    roman_to_int(str)
  end
end

r = Roman.new
r.iv      #=> 4
r.xxiii   #=> 23
r.mm      #=> 2000

method_missing的应用是如此地广泛,以至于只要是成规模的Ruby库,多多少少都会用到它。像是ActiveRecord就是靠这一特性去动态生成关联属性。

其实Python一早就内置了这一功能。Python有一个Magic Method叫__getattr__,它会在找不到属性的时候调用,正好跟Ruby的method_missing是一样的。
我们可以这样动态添加方法:

class MyClass(object):
    def __getattr__(self, name):
        """called only method missing"""
        if name == 'missed_method':
            setattr(self, name, lambda : True)
            return lambda : True

myClass = MyClass()
print(dir(myClass))
print(myClass.missed_method())
print(dir(myClass))

于是乎,前面的Ruby例子可以改写成下面的Python版本:

class Roman(object):
    roman_int_map = {
            "i": 1, "v": 5, "x": 10, "l": 50,
            "c":100, "d": 500, "m": 1000
    }

    def roman_to_int(self, s):
        decimal = 0
        for i in range(len(s), 0, -1):
            if (i == len(s) or
                    self.roman_int_map[s[i-1]] >= self.roman_int_map[s[i]]):
                decimal += self.roman_int_map[s[i-1]]
            else:
                decimal -= self.roman_int_map[s[i-1]]
        return decimal

    def __getattr__(self, s):
        return self.roman_to_int(s)

r = Roman()
print(r.iv)
r.iv #=> 4
r.xxiii #=> 23
r.mm #=> 2000

很有可能你会觉得这个例子没有什么意义,你是对的!其实它就是把方法名当做一个罗马数字字符串,传入roman_to_int而已。不过正如递归不仅仅能用来计算斐波那契数列,__getattr__的这一特技实际上还是挺有用的。你可以用它来进行延时计算,或者方法分派,抑或像基于Ruby的DSL一样动态地合成方法。这里有个用__getattr__实现延时加载的例子

函数对象

在C++里面,你可以重载掉operator (),这样就可以像调用函数一样去调用一个类的实例。这样做的目的在于,把调用过程中的状态存储起来,借此实现带状态的调用。这种实例我们称之为函数对象。

在Python里面也有同样的机制。如果想要存储的状态只有一种,你需要的是一个生成器。通过send来设置存储的状态,通过next来获取调用的结果。不过如果你需要存储多个不同的状态,生成器就不够用了,非得定义一个函数对象不可。

Python里面可以重载__call__来实现operator ()的功能。下面的例子里面,就是一个存储有两个状态value和called_times的函数对象:

class CallableCounter(object):
    def __init__(self, initial_value=0, start_times=0):
        self.value = initial_value
        self.called_times = start_times

    def __call__(self):
        print("Call the object and do something with value %d" % self.value)
        self.value += 1
        self.called_times += 1

    def reset(self):
        self.called_times = 0


cc = CallableCounter(initial_value=5)
for i in range(10):
    cc()
print(cc.called_times)
cc.reset()

伪造一个Dict

最后请允许我奉上一个大脑洞,伪造一个Dict类。(这个可就没有什么实用价值了)

首先确定下把数据存在哪里。我打算把数据存储在类的__dict__属性中。由于__dict__属性的值就是一个Dict实例,我只需把调用在FakeDict上的方法直接转发给对应的__dict__的方法。代价是只能接受字符串类型的键。

class FakeDict:
    def __init__(self, iterable=None, **kwarg):
        if iterable is not None:
            if isinstance(iterable, dict):
                self.__dict__ = iterable
            else:
                for i in iterable:
                    self[i] = None
        self.__dict__.update(kwarg)

    def __len__(self):
        """len(self)"""
        return len(self.__dict__)

    def __str__(self):
        """it looks like a dict"""
        return self.__dict__.__str__()
    __repr__ = __str__

接下来开始做点实事。Dict最基本的功能是给一个键设置值和返回一个键对应的值。通过定义__setitem____getitem__方法,我们可以重载掉[]=[]

    def __setitem__(self, k, v):
        """self[k] = v"""
        self.__dict__[k] = v

    def __getitem__(self, k):
        """self[k]"""
        return self.__dict__[k]

别忘了del方法:

    def __delitem__(self, k):
        """del self[k]"""
        del self.__dict__[k]

Dict的一个常用用途是允许我们迭代里面所有的键。这个可以通过定义__iter__实现。

    def __iter__(self):
        """it iterates like a dict"""
        return iter(self.__dict__)

Dict的另一个常用用途是允许我们查找一个键是否存在。其实只要定义了__iter__,Python就能判断if x in y,不过这个过程中会遍历对象的所有值。对于真正的Dict而言,肯定不会用这种O(n)的判断方式。定义了__contains__之后,Python会优先使用它来判断if x in y

    def __contains__(self, k):
        """key in self"""
        return k in self.__dict__

接下要实现==的重载,不但要让FakeDict和FakeDict之间可以进行比较,而且要让FakeDict和正牌的Dict也能进行比较。

    def __eq__(self, other):
        """
        implement self == other FakeDict,
        also implement self == other dict
        """
        if isinstance(other, dict):
            return self.__dict__ == other
        return self.__dict__ == other.__dict__

要是继续实现了__subclass____class__,那么我们的伪Dict就更完备了。这个就交给感兴趣的读者自己动手了。

Blast-2.2.29编译安装方法(适用于Linux)

官网的帮助文档中没有提到如何从源代码编译出Blast,只是提供了各主流平台下二进制版本的安装方式。
不过好在官网中提到了下载源代码的地点,当你把源代码下下来之后,编译它并不困难。
官网下有个ftp的站点(就是你下各种二进制版本的地方), 首先从那里下载源代码。在这里我下的是ncbi-blast-2.2.29+-src.tar.gz这个文件。
解压它,在c++目录下有一个configure文件。就像编译其他软件一样,先运行configure,然后它会检查各种前置条件。最后会提醒你使用make来开始编译Blast。
于是照着它的告示,make一下。

接着你有两个选项:

  1. 去看电影
  2. 出去玩

大概2个小时后编译结束,在pathtoblast/c++/ReleaseMT/bin下会生成各种二进制可执行文件。把这个目录加入到PATH中,你就可以使用Blast的各种工具了。

测试是否安装成功的方法,就跟测试二进制版本是否能运行一样:

blastdbcmd -db refseq_rna.00 -entry nm_000249 -out test_query.fa
# 从数据库文件refseq_rna.00中提取出条目nm_000249,存储到test_query.fa中
# 当然你需要从ftp那里下载refseq_rna.00这个数据库。具体下载方式看各个二进制版本的安装方法末尾的地方。

blastn -query test_query.fa -db refseq_rna.00 -task blastn -dust no -outfmt "7 qseqid sseqid evalue bitscore" -max_target_seqs 2
# 搜索条目nm_000249,如果能够找出来,就是安装成功了。

Mac上面应该也能同理安装。Win上面就不知道了……

更新:
在上网搜索Blast的算法的时候,偶尔发现了这么个重要的东西:
pathtoblast/src/algo/blast/core/README里面交代了在各个平台下编译Blast的方式。
这里就直接把它的内容转发如下,以作补充:(其实看了这个README,就可以忘掉前面我写的内容了。)

Getting the source code

Download the source distribution of BLAST+:
ftp://ftp.ncbi.nlm.nih.gov/blast/executables/LATEST/ncbi-blast-VERSION+-src.tar.gz

Build instructions

Unpack the source archive in its installation directory and change working
directory to ncbi-blast-VERSION+-src/c++.

UNIX:
To build these source files into a library without the rest of the NCBI BLAST+
applications/libraries, one should use the following commands:

./configure --with-projects=scripts/projects/blast_core_lib.lst
--without-debug --with-mt --with-build-root=ReleaseMT
cd ReleaseMT/build
make all_p

This will configure and build an optimized library called blast, which can then
be referenced in makefiles as follows:

NCBI_HOME=<installation directory of the NCBI C++ toolkit>
-I$NCBI_HOME/c++/ReleaseMT/inc -I$NCBI_HOME/c++/include
-L $NCBI_HOME/c++/ReleaseMT/lib

Windows:

  1. Open the ncbi_cpp.sln project/solution file
    c++/compilers/msvc800_prj/static/build/ncbi_cpp.sln.
  2. Right click on the -CONFIGURE-DIALOG- project on the Solution Explorer and
    select "Build" from the context menu, which will bring up a window titled
    "Project Tree Builder".
  3. In the "Project Tree Builder" window's first text box, enter
    scripts\projects\blast_core_lib.lst, click OK, and on the subsequent window
    click "Reload".
  4. After the environment reloads, right click on blast.lib and select "Build".

The blast.lib library file will be found in
c++\compilers\msvc800_prj\static\lib\CONF\blast.lib, where CONF represents the
appropriate configuration (e.g.: debugdll, debugmt, releasedll, or releasemt),
and the headers will be found in c++\compilers\msvc800_prj\static\inc and
c++\include

chrome devtool 技巧 之 杂项

这一次讲的是关于network、profile等内容。简而言之,就是讲各种杂项。既然讲的是杂项,说的也自然难以找到一个主题。而且对于这些面板,实际上自己也没怎么用过,所以恐怕会讲得枯燥无味些。

network面板

没什么好说的……左边是请求的各项资源,右边是请求、响应所花费的时间。右边这一栏中,浅色的为浏览器请求花费的时间,深色的为服务器响应花费的时间。蓝色线为DOMContented事件发生的时间(就是DOM tree中各个元素已经各就各位的时候),这时候会触发JQuery中的$(document).ready()。而红色线是真正的load事件发生的时间,这时候各项资源已经加载完毕了。

有一个小技巧:在请求图标上右键菜单,可以选择copy as cURL,则能复制出对应的curl请求。(curl党有福了)

timeline面板

可以录制某段时间内发生的浏览器事件,比如render、scroll之类。并显示其对应的开销。
在这里可以清晰地显示浏览器在进行解析html之类的操作所花费的时间和消耗的CPU资源等等。优化强迫症患者必备。

另附:用鼠标滚轮可以放大/缩小所在的区域。

profile面板

点击录制按钮,开始录制CPU开销,或者给heap上分配的内存来个快照。

CPU开销这个基本没有过。给heap上分配的内存拍快照,则有些用途,据说可以查明JS内存泄露的问题。

JS内存泄露

在V8中,除了普通的Number类型(4个byte)和某些特殊的存储在JS VM之外的对象,其他所有的对象都分配在heap上。
这些被分配的对象互相引用,构成了一个图。

假如我们把某个对象当做GC的根对象,那么就可以给其他对象设定一个距离。而所有的互相引用的对象也可以看做一棵树。

不过这里不需要懂得多少概念,因为我们很快就要进入主题 —— JS的内存泄露 —— 了。

如果我们定义内存泄露为“一定时间内存在无法访问但是占据了内存的对象”, 那么有GC的语言也是会发生内存泄露的。

当某些对象不再被其他对象引用时,那么在下一次GC来临之际,它们就会被回收。而如果本应该回收的对象(不再被之后的代码所访问),被某些对象隐式引用了,那么就会导致这个对象不能被回收。这样就导致了内存泄露的问题。

而在JS里,一般这可能会是DOM元素引用或者绑定事件函数到某个不能访问的DOM元素所造成的。

这里举个例子:

varselect=document.querySelector; 
vartreeRef=select("#tree"); 
varleafRef=select("#leaf"); 
varbody=select("body");
body.removeChild(treeRef); //#tree can't be GC yet due to treeRef
treeRef = null;  //#tree can't be GC yet due to indirect  reference from leafRef  
leafRef = null;  //#NOW can be #tree GC

简而言之,不能长期持有对大对象的子节点的引用。因为只要持有这样一个引用,该引用涉及的子节点的parentNode会逐级指向该大对象的根节点,就无法回收整个大对象了。

毕竟这不是一篇讲JS内存泄露的文章,所以感兴趣的读者可以转到下面两个外部资源:

了解 JavaScript 应用程序中的内存泄漏
js的闭包和回调到底怎么才会造成真的内存泄漏呢?

生成报告

注意:profile面板生成的报告需要手动移除(比如点击那个__禁止__标记)。否则再次打开面板那个报告还是继续存在

报告有几种显示的选项,其中:
summary是默认选项,显示heap上的内存分配情况。
comparison可以显示两个报告之间的变化。

我们主要看一下报告的格式:

从左到右各列为:

  • Constructor 构造函数
  • distance ​ 距根节点的距离
  • Objects Count 个数
  • Shallow Size 占用内存
  • Retained Size 自身加引用的节点总共占用的内存

其中背景为黄色的元素是被JS引用的DOM元素,红色背景的元素是已经被移除但还未被回收的,比如上面例子中的#tree。

具体怎么发掘出内存泄露的问题?这篇文章基本上把具体步骤给讲清楚了,我就不转译了。

乐园追放

最近在朋友的推荐下看了《乐园追放》,号称是“老虚从良之作”,从头到尾没死过人……

当然我不是来看没死人的。之所以朋友会推荐这个电影,是因为当时我们在讨论《文明:太空》中的三条线:至高、纯正和和谐,然后他提到“有部电影的主题也是讲这三条道路”。于是今天中午,趁着有空,我放弃午休看了下这部片子。感觉这部电影的确值得一荐。

以下内容具有剧透,请自行斟酌是否继续阅读!

大致看法

设定一般,大概就是普通动画片和科幻片的套路。像是《群星的尽头》和《与拉玛相会》的混合。当然,编剧不太可能是因为受这两本小说影响,毕竟科幻的领域里,大部分的创意都被前人的作品挖掘光了,各种作品间难免能看到彼此的影响。

不过本片的优点在于细节和剧情、人设。人物的一言一语、一举一动,都能体现出动画制作者的精心设计。而剧情方面,安吉拉和野狗,还有安吉拉、野狗、拓荒者三位主人公之间的交互都令人记忆尤深。特别是主题曲EONIAN响起来的时候,顿时给人一种苍凉的沉重感。编剧在各种情节间的切换很在行,紧紧抓住了观众的心。

至于人设,三位主人公的形象已经深入我心。好强又果敢的安吉拉,机智而爱好自由的野狗,半个人半个机器、古怪又可爱的拓荒者……

当然我才不会说,女主的身材也是个看点(pia飞)

乐园

天上有一个乐园,有人享受里面的生活,有人拒绝进入,有人想要追求新的乐园。
这就是《乐园追放》的三条道路。

当安吉拉奇怪野狗为什么一再拒绝前往乐园的邀请时,野狗认为,所谓的乐园,其实是骗人的。在乐园里的人,仍然免不了为有限的记忆体而争名逐利;如果有人为社会所不容,那么他就完蛋了,会被永远档案化。甚至还存在保安局这样的**机构。

当安吉拉和野狗惊讶于,拓荒者为了准备发射飞船到外星空间,已经花了上百年时间。拓荒者说,这是为了证明我自己的存在。

当安吉拉因为拓荒者求情而被保安局高层囚禁时,拓荒者出于仁义,前往解救安吉拉。两人从天上杀到地上。之后安吉拉也出于仁义,坚守发射基地,阻击迪瓦派来的来兵。

拓荒者问安吉拉是否愿意跟它同行。安吉拉当时正骑着她的机甲在空中激战。她俯瞰了下大地,回答说:“对于这个世界,我还有很多地方没有去探索,很多事情没有去了解”。最后决定留在地球。

最后,因为迪瓦中没有人回应共同踏上新的征途的邀请,拓荒者想要中断自己的计划。野狗和安吉拉告诉他,拓荒者虽然不是人,但也算是人类的一员。即使无人陪伴,也应该勇敢地走出去,向外星宣扬人类的功绩。拓荒者终于决定独自出发,驶往无尽的星海。

It is so far away

看完电影后,我找来主题曲听了一遍又一遍。只是没有了电影的氛围,主题曲的魅力折损不少。但是听到了It is so far away这一句时,心中难免感慨万千。

这首主题曲,感觉还是片中野狗大叔就着吉他弹奏的版本最好听,因为这一版本最能体现一股苍凉的感觉。特别是片尾,拓荒者独自踏上茫茫旅程时,它唱着It is so far away,顿时泪点都快上来了。

科幻片的氛围,大体就是这一种感觉吧。

什么样的contributions会被Github计算在内?

在热衷于在Github上刷contributions的人(比如我)看来,每周看着contributions涨涨涨,看着Contributions Calendar越来越绿意盎然,心里一股幸福感油然而生。

当然,这种心理现象就像LOL玩家喜欢看排名和胜盘一样,是病,得治。

有些时候,这些平凡而坚定的人,这些脱离了低级趣味的人,这些将有限的一生奉献到无限的为人类的爱与和平打代码的事业的人……惊奇地发现,自己push的commit没有被算入contributions里。或者自己赶在截止时间之前,辛辛苦苦地完成一个commit,结果发现算到新的一天里,然后苦心策划的连击就这样前功尽废了。这时候,so sad, so painful。

这种情绪是不好的,因为它意味着原本坚持打代码的朴素愿望已然变质,被外部化成为contributions的增加而打代码,失去了打代码的本意……不过要是没有这种contributions作为一种满足收集癖的动力,恐怕连坚持打代码的原本的朴素愿望也不会产生呢。这么说来真是说不清啊。好吧,让我们跳过纯粹道德批判,来看看主题:什么样的contributions会被Github计算在内?

为了弄清楚这个问题,避免悲剧的一再重演,我决定查看Github的相关政策法规,并且整理整理。现在就跟大家分享一下。

并非所有的contributions是生而平等的

正如人们所熟知,人生而不平等,同样的道理可以应用在contributions上。至少在Github上,有些contributions就是那么不幸。

contributions的创建时机

一个contributions被创建,需要满足“新建一个issue” || “发出一个pull request” || “创建一个commit”。

然而,如果一个contributions是通过创建commit来产生的,那么它就会面临生来的不平等。

尤其当你是一个commit contributions

commit contributions分为两种:

  1. 向一个不是你fork的版本库commit
  2. 向一个你fork的版本库commit

对于前者,必须是在默认的分支(一般为master)或者在gh-pages(就是当你直接在github上edit的时候创建的)分支上的commit才算数。

对于后者,要等到上游的人把代码merge到上游的默认的分支,你的commit才会被计算。如果你的comit没有被merge,那么自然就不会算数了。

当然还有些情况下不会计算到你的contributions里面的,比如你的commit记录是一年多以前的,或者那个commit的创建者没有跟你的Github帐号联系起来,等等。

BTW,对私有仓库的commit也是会被考虑在内的。具体的处理方式应该跟公有仓库是一样的。

contributions calendar什么时候开始新的一天?

貌似这个contributions calendar上新的一天的开始时间,曾经变过几次……不过一如《1984》所说,人们总是容易忘掉过去的状况,所以我也不太记得曾经是什么时候开始新的一天的,大概是下午吧。不过现在是跟当地时间是同步的。再也不用担心错过commit的时机了。

最后提醒一下,contributions机制可以鼓励人们坚持打代码,但是不要让自己写代码的激情被contributions给外部化了。
无论你的commit算不算数,能够为自己的项目添加新的功能,或者解决已有的bug,才是编程的目标。

我需要一个可以绑定DOM的SPA框架

在用Backbone两次之后,我终于无法忍受它的原始和简陋。虽然Backbone不是一个SPA框架,而只是个javascript的MVC框架罢了。以SPA框架去强求它,的确是不公平的。不过,我还是渴望用一个可以绑定DOM的SPA框架。

在用Backbone编程的时候,你需要把代码划分为model和view两层。虽然Backbone声称是MVC(Model View Collection)框架,但是Collection其实也应该划分到Model类别中。

Model层负责放置数据,而view层负责展示数据和接受用户的操作事件。在这里,view层相当于一般的MVC架构中controller加view的组合。当然,在这样的组合中,就不要指望将view和controller分离了。不过这个不是我现在要吐槽的问题。问题是,controller无法自动地更新view。

让我们看看Backbone中model和view是怎么协作的。首先view接收事件,然后修改绑定的model,再用model的数据更新页面(或者由model驱动被绑定的view更新)。这样一来,controller的更新,就依赖于model层对脏数据的处理了。model层在产生脏数据之后,必须去触发view层更新。如果没有及时更新,就会发生数据混乱的问题。所以,model层需要紧密地和view层绑定。每次model的数据改变后,都需要要相应的方法来改变对应的页面内容。这个方法既可以放在model层中,也可以放在view层之中。

结果是,对应于model的每个状态,都需要有对应的方法来更新页面。这么说来你需要写两套方法,一套方法来响应用户对页面的动作,并反馈给受绑定的model;另一套方法用于使用model层最新的数据,更新页面。哦对了,你还要手工绑定model和view,有时还需要双向绑定喔。这么多方法,不但写起来麻烦,维护起来也麻烦。

如果SPA框架可以绑定具体的DOM,然后由此实现对应的MVC,那该多好呀!

这么一来,不但立刻有了现成的接口可用,而且SPA带来的页面不会刷新的问题也能解决了。再也不需要在view层中显式记录当前DOM的状态了,只需要交由框架自动处理了。view层的职责又能卸下一大块。

如果能更进一步,以前端的每一个model对应后台的每一个model,然后可以像定义路由表一样定义model的方法,调用该方法自动调用同名的RESTful 方法,那就更加方便了,不再需要依靠少的可怜的几个同步方法,不再手工写$.ajax了。

devdocs添加新文档

本篇是“dash/zeal添加新文档”的姐妹篇,讲讲如何在另一个类似的文档应用——devdocs——中添加文档。

如果你之前没有用过类似的文档应用,建议现在就用起来。它们可以显著地提高文档查找的效率,而查文档又是日常编程中常做的事情之一。

devdocs 介绍

devdocs是一个开源的文档浏览应用。跟dash一样,它把HTML源文档转换成带索引的docset规格,以供用户查看。不过devdocs是一个用rails写的web应用而非本地应用,文档作为静态资源由服务器提供,交互则是通过浏览器页面完成。你可以在devdocs.io使用该应用,也可以在自己的电脑上部署它。注意devdocs提供了offline的选项,可以把数据写入到浏览器的indexeddb中,避免了每次从服务器获取数据带来的延迟。

在制作docset方面,devdocs跟dash最为显著的不同,在于devdocs把制作docset的功能集成到了应用当中。跟dash不同,devdocs把docset规格留作黑箱,取代它的是开发出一组制作docset的接口,通过这些接口制作的docset才能被识别。这么做的好处在于,官方会提供了制作docset的通用的辅助函数,减少了自己的开发量。坏处呢,就是限制了制作docset的发挥空间。另外,由于docset的静态资源代码是放在应用代码里面的,导致分发docset时也要将对应的静态资源代码一并分发。考虑到devdocs主要是一个web应用而非本地应用,这么做也不难理解。

还是跟上一篇讲dash的一样,本篇依旧采用官方文档解读加示例的形式。由于devdocs假定docset都位于同一个入口地址下(比如xxx/docs/),而上一篇的OpenResty文档不符合这一假设,所以换个示例。这次我们选择用devdocs里面的PHP作为示例。注意devdocs是一个Rails应用,所以要用它提供的接口自然只能用Ruby。不过据我观察,要制作docset并不需要什么Ruby编程知识。即使不懂Ruby,你也可以拿起现有的代码改改看,然后搜一下相关的语法知识,就能完事。

devdocs添加新文档(PHP示例)

官方文档见:https://github.com/Thibaut/devdocs/wiki/Adding-documentations-to-DevDocs

创建一个文档需要以下几步(假设需要生成的文档名为docset_name,下面的路径都是相对于devdocs项目代码根目录的相对路径,再强调一次,给devdocs添加文档需要在项目中写拓展代码):

  • ./lib/docs/scrapers/下创建一个scraper类,起名为docsetName(注意这里用的是驼峰命名法)。让它继承自Docs::UrlScraper(用于下载在线文档)或Docs::FileScraper(用于处理本地文档,仅当文档页面过多时推荐)。
  • 填写该类的以下属性。
# https://github.com/Thibaut/devdocs/blob/master/lib/docs/scrapers/php.rb
module Docs
  class Php < FileScraper
    include FixInternalUrlsBehavior
    self.name = 'PHP' # 文档名
    self.type = 'php' # 样式/语法高亮js类的的名字
    self.release = 'up to 7.0.3' # 版本号
    # 源文档入口地址。
    self.base_url = 'https://secure.php.net/manual/en/'
    self.root_path = 'index.html' # 根目录页
    # 各子部分文档入口。下面的语法糖相当于`initial_paths = ['funcref.html', ...]`
    self.initial_paths = %w(
      funcref.html
      langref.html
      refs.database.html
      set.mysqlinfo.html
      language.control-structures.html
      reference.pcre.pattern.syntax.html
      reserved.exceptions.html
      reserved.interfaces.html
      reserved.variables.html)

    self.links = {
      home: 'https://secure.php.net/',
      code: 'https://github.com/php/php-src'
    }

    # 由于该类继承自filescraper,所以从该路径下查找源文档。
    self.dir = '/Users/Thibaut/DevDocs/Docs/PHP'
    # 每个scraper有两个filter栈,第一个是html,另一个是text
    # html filter把文档当做Nokogiri节点处理。
    html_filters.push 'php/internal_urls', 'php/entries', 'php/clean_html', 'title'
    # text filter把文档当做字符串处理。
    text_filters.push 'php/fix_urls'
    # 关于Filter的更多内容,下面再一一道来。

    # options用于控制不同类型的filter的行为。具体见
    # https://github.com/Thibaut/devdocs/wiki/Scraper-Reference#filter-options
    options[:title] = false
    # 以下略
  end
end
  • ./lib/docs/filters/下创建一个./lib/docs/filters/<docset_name>/文件夹,将会在里面放置你的filter类。

  • devdocs在下载页面的时候,除了默认的filter(如AttributionFilter)外,还会调用你的filter类来处理数据。所以你需要准备下面两个filter。

    • CleanHtmlFilter 该filter用于清理掉跟文档内容无关的页面元素。
      相应的代码位于 https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/php/clean_html.rb
      思路是使用nokogiri来删除/插入/修改文档的页面元素,去掉无用的,部分位置进行微调。
    • EntriesFilter 该filter用于提取文档内容,得到条目,要求实现get_name/get_type/include_default_entry?/additional_entries四个方法。每个条目有三个属性:nametypepathname会用于索引,要求在整个文档中不能重复。type则是该条目所在的归类,如Function等等。path则是该条目所在的源文档路径。devdocs约定每个页面代表一个条目,get_name/get_type分别返回它的名字和类型,而path则默认为该页面的路径。如果一个页面有多个文档怎么办?那么需要剩下两个方法配置下。include_default_entry?默认返回true,表示当前页面就是单独一个条目(所谓的默认条目);如果当前页面只是若干条目的集合,覆盖该方法返回false。这时需要覆盖additional_entries方法,返回一个[[name,fragment_id, type], ...]数组,表示该页面中“额外的”(非默认)条目,其中fragment_id取该条目在页面中的相对锚点。举个例子,如果文档中有一个类似这样的条目:
    <a href="#method-foo"></a>
    <h3>foo</h3>
    

    那么返回的数组应包括['foo', 'method-foo', 'method']

    相应的代码位于 https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/php/entries.rb
    思路简单粗暴,就是用nokogiri的接口获取该页面的列出的条目的名字和目录,然后通过一个硬编码的哈希表查出其所属的类型。由于PHP文档每个条目单独成页,所以只需覆盖前两个方法。

    另外,我们也可以看到示例代码里面自定义了其他几个filter,用于重写文档中的url。
    关于Filter的更多内容,见文档

  • 还需要给文档准备一份样式。在assets/stylesheets/pages/下创建_[type].scss,并在application.css.scssimport它。这里type指scraper类中的type属性的值。这个文件中的样式需要位于名为_[type]的类。

# https://github.com/Thibaut/devdocs/blob/master/assets/stylesheets/pages/_php.scss
# 样式什么的不需要跟源文档的一致,只要看起来舒服就好
._php {
    ...
}
  • 运行thor docs:page <docset_name> <path>下载文档到./public/docs/<docset_name>/。确保能够成功下载文档。
  • 运行thor docs:generate <docset_name> --force --verbose --debug生成文档。
  • 现在你可以刷新下应用,看看生成的文档。第一次尝试总是会有些问题的。调整下样式、scraper代码、filter代码,重复上面两步,直到输出满意的结果为止。
  • 如果你的docset需要语法高亮支持,在./assets/javascripts/views/pages/创建一个coffeescript脚本。直接拿现有的改就好:
# https://github.com/Thibaut/devdocs/blob/master/assets/javascripts/views/pages/php.coffee
#= require views/pages/base
# 注意类的名字为app.views.[type]Page
class app.views.PhpPage extends app.views.BasePage
  prepare: ->
    # 用php语法,高亮.phpcode里面的内容
    @highlightCode @findAllByClass('phpcode'), 'php'
    return

chrome devtool 技巧 之 elements

Elements可能是大家见过次数最大的面板了,因为每次审查元素都会打开它。
所以这一次来讲讲Elements面板中可用的小技巧。

Elements面板可以分成两块,左边的显示html源码,右边的显示各种相关的属性,默认是显示对应dom元素的style。

那么先从左边开始:

html源码框

右键菜单,你能看到许多有用的选项。复制、将选择的dom元素置于某种状态什么的不说了。有一个“Scroll into view”选项,可以将页面滚动到鼠标所在/选中的DOM节点上。还有一个“Break on”,弹出子菜单中可以选择断点类型。可以选择DOM元素发生何种变化时会触发一个断点。该断点一旦被触发,就会跳转到Source面板中,进入debug模式。具体有哪些类型,菜单上已经写得一目了然了,这里不再赘述。

style框

看完左边的html源码框,让我们出门右转进入右边的style框。这个框显示的是选中的dom节点的具体css style。自上而下,分别是不同的css规则,按优先级先后排列。

一个重要的技巧是,如果你点击css右上角的url链接,就能跳转到Source面板中对应的文件。在这里Ctrl+s能够保存对css文件的修改。这意味着,如果在调试的过程中对css规则做了较大的修改,不需要跑到各个css规则中复制修改到源文件中,只需要跳到对应的Source面板下的css源文件中进行保存就行了。

还有一个有趣的地方,在颜色选择器中Shift+click,会改变颜色的表示方式,比如从#FFFFFF变为rgb(255,255,255)。对于强迫症患者,这个倒是个不错的功能呢。

Compute框

在Style框中查看DOM元素属性时,有时候会发现自己想要查明的属性值,居然要拉到最底下才看到,原来是被XX规则设定成这样了。

其实切换到隔壁Compute框就不用那么辛苦了。

Compute框中显示的是选中元素计算出来的最终属性。勾上Show inherited properties并配合Filter一起使用,生活一下子轻松了很多。

Event Listener框

如其名。这个框显示的是相关的event listeners。展开对应的event listener,可以看到若干个属性,其中有isAttribute,显示是否是通过DOM属性(如on系列)来设置的;还有useCapture,显示该addEventListener调用中设置的useCapture 值。其他还有别的什么属性,因为没有用到过,所以我直接忽略掉好了。

当然最重要的是右上角的url,点开它直达对应的源码片段。

有一个问题是,该框中默认显示所有的event listeners。好在在框的最上方有一个漏斗,点击下可以选择“Selected Node Only”,就只显示当前选中元素的event listeners。这在调试中用处非常大,是不是顿时感觉生活轻松多了呢?

下期预告:接下来我会写点chrome devtool中关于network(请求,响应)和profile之类的小技巧。

跟我一起写shell补全脚本(Bash篇)

上一篇里我们定下了给pandoc写补全脚本的计划:

  1. 支持主选项(General options)
  2. 支持子选项(Reader options/General writer options)
  3. 支持给选项提供参数值来源。比如在敲pandoc -f之后,能够补全FORMAT的内容。

支持主选项

先列出实现了第一阶段目标的程序:

# 以pandoc的名字保存下面的程序
_pandoc() {
    local pre cur opts

    COMPREPLY=()
    #pre="$3"
    #cur="$2"
    pre=${COMP_WORDS[COMP_CWORD-1]}
    cur=${COMP_WORDS[COMP_CWORD]}
    opts="-f -r -t -w -o --output -v --version -h --help"
    case "$cur" in
    -* )
        COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
    esac
}
complete -F _pandoc -A file pandoc

运行程序的方式:

$ . ./pandoc # 加载上面的程序
$ pandoc -[Tab][Tab] # 试一下补全能用不

现在我来解释下这个程序。

complete -F _pandoc -A file pandoc

是这段代码中最为关键的一行。其实该程序起什么名字都不重要,重要的是要有上面这一行。上面这一行指定bash在遇到pandoc这个词时,调用_pandoc这个函数生成补全内容。(叫_pandoc其实只是出于惯例,并不一定要在前面加下划线)。complete -F后面接一个函数,该函数将输入三个参数:要补全的命令名、当前光标所在的词、当前光标所在的词的前一个词,生成的补全结果需要存储到COMPREPLY变量中,以待bash获取。-A file表示默认的动作是补全文件名,也即是如果bash找不到补全的内容,就会默认以文件名进行补全。

假设你在键入pandoc -o sth后,连击两下Tab触发了补全,_pandoc会被执行,其中:

  1. $1的值为pandoc
  2. $2的值为sth
  3. $3的值为-o
  4. 由于COMPREPLY为空(只有cur-开头时,COMPREPLY才会被填充),所以补全的内容是当前路径下的文件名。

你应该看到了,这里我把$2$3都注释掉了。其实

pre="$3"
cur="$2"

pre=${COMP_WORDS[COMP_CWORD-1]} # COMP_WORDS变量是一个数组,存储着当前输入所有的词
cur=${COMP_WORDS[COMP_CWORD]}

是等价的。不过后者的可读性更好罢了。

最后解释下COMPREPLY=( $( compgen -W "$opts" -- $cur ) )这一行。
opts就是pandoc的主选项列表。
compgen接受的参数和complete差不多。这里它接受一个以IFS分割的字符串"$opts"作为补全的候选项(IFS即shell里面表示分割符的变量,默认是空格或者Tab、换行)。假如没有一项跟当前光标所在的词匹配,那么它返回当前光标所在的词作为结果。(也即是不补全)

实现第一个目标用到的东西就是这么多。接下来就是第二个目标了。
在继续之前,你需要把Bash文档看一遍。若能把其中的一些选项尝试一下就更好了。

支持子选项

接下来的目标是支持Reader options/General writer options。想判断是否需要补全Reader options/General writer options,先要确认输入的词里面是否有-r-f(读),以及-w-t(写)。前面提到的COMP_WORDS就派上用场了。只需要将它迭代一下,查找里面有没有我们需要确认的词。

假设我们已经确认了需要补全子选项,接下来就应该往原来的补全项中添加子选项的内容。需要补全读选项的添加读方面的选项,需要补全写选项的添加写方面的选项。既然补全选项是一个字符串,那么把要添加的字符串接到原来的opts后面就好了。这里要注意一点,假如前面的操作里面已经把某类子选项添加到opts了,那么就需要避免重复添加。

目前的实现代码如下:

_pandoc() {
    local pre cur

    COMPREPLY=()
    #pre="$3"
    #cur="$2"
    pre=${COMP_WORDS[COMP_CWORD-1]}
    cur=${COMP_WORDS[COMP_CWORD]}
    complete_options() {
        local opts i
        opts="-f -r -t -w -o --output -v --version -h --help"
        for i in "${COMP_WORDS[@]}"
        do
            if [ "$i" == "-f" -o "$i" == "-r" ]
            then
                opts="$opts"" -R -S --filter -p"
                break
            fi
        done

        for i in "${COMP_WORDS[@]}"
        do
            if [ "$i" == "-t" -o "$i" == "-w" ]
            then
                opts="$opts"" -s --template --toc"
                break
            fi
        done
        echo "$opts"
    }

    case "$cur" in
    -* )
        COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur ) )
    esac
}
complete -F _pandoc -A file pandoc

注意跟上一个版本相比,这里把原来的opts变量替换成了complete_options这个函数的输出。通过使用函数,我们可以动态地提供补全的来源。比如我们可以在函数里列出符合特定条件的文件名,作为补全的候选词。

支持给选项提供参数值来源

好了,现在是最后一个子任务。大致浏览一下pandoc的文档,基本上就两类参数:FORMATFILE。(其它琐碎的我们就不管了,嘿嘿)

FILE好办,默认就可以补全路径嘛。那就看看FORMATFORMAT分两种,一种是读的时候支持的FORMAT,另一种是写的时候支持的FORMAT,这个把文档里面的复制一份,改改就能用了。我们把读操作支持的FORMAT叫做READ_FORMAT,相对的,写操作支持的FORMAT叫做WRITE_FORMAT

补全的来源有了,想想什么时候把它放到COMPREPLY里去。前面补全选项的时候,是通过case语句中-*来匹配的。但是这里的FORMAT参数,只在特定选项后面才有意义。所以前面一直坐冷板凳的pre变量可以上场了。

pre中存储着光标前一个词。我们就用一个case语句判断前面是否是-f-r,还是-t-w。如果符合前面两个组合之一,用compgen配合READ_FORMATWRITE_FORMAT生成补全候选词列表,一切就跟处理opts时一样。由于此时继续参与下一个判断cur的case语句已经没有意义了,这里直接让它退出函数:

READ_FORMAT="native json markdown markdown_strict markdown_phpextra 
    markdown_github textile rst html docbook opml mediawiki haddock latex"
WRITE_FORMAT="native json plain markdown markdown_strict 
    markdown_phpextra markdown_github rst html html5 latex beamer context 
    man mediawiki textileorg textinfo opml docbook opendocument odt docx 
    rtf epub epub3 fb2 asciidoc slidy slideous dzslides revealjs s5"

case "$pre" in
-f|-r )
    COMPREPLY=( $( compgen -W "$READ_FORMAT" -- $cur ) )
    return 0
    ;;
-t|-w )
COMPREPLY=( $( compgen -W "$WRITE_FORMAT" -- $cur ) )
    return 0
esac

. ./pandoc一下,试试看,是不是一切都ok?

诶呀,还有个问题!这次在尝试补全FORMAT的时候,还会把当前路径下的文件名补全出来。然而这并没有什么意义。所以在补全FORMAT的时候,得把路径补全关掉才行。

问题在于最后一句:complete -F _pandoc -A file pandoc。目前不管是什么情况,都会补全文件名。所以接下来得限定某些情况下才补全文件名。

第一步是移除最后一行的-A file,下一步是修改最底下的case语句,变成这样子:

case "$cur" in
-* )
    COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur ) );;
* )
    COMPREPLY=( $( compgen -A file ))
esac

只有在没有找到对应的补全时,才会调用对路径的补全。

最终版本:

_pandoc() {
    local pre cur

    COMPREPLY=()
    #pre="$3"
    #cur="$2"
    pre=${COMP_WORDS[COMP_CWORD-1]}
    cur=${COMP_WORDS[COMP_CWORD]}
    READ_FORMAT="native json markdown markdown_strict markdown_phpextra 
    markdown_github textile rst html docbook opml mediawiki haddock latex"
    WRITE_FORMAT="native json plain markdown markdown_strict 
    markdown_phpextra markdown_github rst html html5 latex beamer context 
    man mediawiki textileorg textinfo opml docbook opendocument odt docx 
    rtf epub epub3 fb2 asciidoc slidy slideous dzslides revealjs s5"

    case "$pre" in
    -f|-r )
        COMPREPLY=( $( compgen -W "$READ_FORMAT" -- $cur ) )
        return 0
        ;;
    -t|-w )
        COMPREPLY=( $( compgen -W "$WRITE_FORMAT" -- $cur ) )
        return 0
    esac

    complete_options() {
        local opts i
        opts="-f -r -t -w -o --output -v --version -h --help"
        for i in "${COMP_WORDS[@]}"
        do
            if [ "$i" == "-f" -o "$i" == "-r" ]
            then
                opts="$opts"" -R -S --filter -p"
                break
            fi
        done

        for i in "${COMP_WORDS[@]}"
        do
            if [ "$i" == "-t" -o "$i" == "-w" ]
            then
                opts="$opts"" -s --template --toc"
                break
            fi
        done
        echo "$opts"
    }

    case "$cur" in
    -* )
        COMPREPLY=( $( compgen -W "$(complete_options)" -- $cur) )
        ;;
    * )
        COMPREPLY=( $( compgen -A file ))
    esac
}
complete -F _pandoc pandoc

最后的问题

现在补全脚本已经写好了,不过把它放哪里呢?我们需要找到这样的地方,每次启动bash的时候都会自动加载里面的脚本,不然每次都要手动加载,那可吃不消。

.bashrc是一个(不推荐的)选择,不过好在bash自己就提供了在启动时加载补全脚本的机制。

如果你的系统有这样的文件夹:/etc/bash_completion.d,那么你可以把补全脚本放到那。这样每次bash启动的时候就会加载你写的文件。

如果你的系统里没有这个文件夹,你需要查看下/etc/bash_completion这个文件。bash启动的时候,会执行. /etc/bash_completion,你可以把你的补全脚本放在这个地方。

正如许多配置文件一样,凡是有/etc版本的也对应的~/.版本。有/etc/bash_completion,自然也有~/.bash_completion。如果你只想让自己使用这个补全脚本,或者没有root权限,可以放在~/.bash_completion

Bash补全脚本的内容就是这么多……请期待下一篇的Zsh补全脚本。

[译]Linux性能分析的前60000毫秒

原文链接:http://techblog.netflix.com/2015/11/linux-performance-analysis-in-60s.html
作者是Brendan Gregg, Oracle/Linux系统性能分析方面的大牛。

Linux性能分析的前60000毫秒

为了解决性能问题,你登入了一台Linux服务器,在最开始的一分钟内需要查看什么?

在Netflix我们有一个庞大的EC2 Linux集群,还有非常多的性能分析工具来监控和调查它的性能。其中包括用于云监控的Atlas,用于实例按需分析的Vector。即使这些工具帮助我们解决了大多数问题,我们有时还是得登入Linux实例,运行一些标准的Linux性能工具来解决问题。

在这篇文章里,Netflix Performance Engineering团队将使用居家常备的Linux标准命令行工具,演示在性能调查最开始的60秒里要干的事,

最开始的60秒......

运行下面10个命令,你可以在60秒内就对系统资源的使用情况和进程的运行状况有大体上的了解。无非是先查看错误信息和饱和指标,再看下资源的使用量。这里“饱和”的意思是,某项资源供不应求,已经造成了请求队列的堆积,或者延长了等待时间。

uptime
dmesg | tail
vmstat 1
mpstat -P ALL 1
pidstat 1
iostat -xz 1
free -m
sar -n DEV 1
sar -n TCP,ETCP 1
top

有些命令需要你安装sysstat包。(译注:指mpstat, pidstat, iostat和sar,用包管理器直接安装sysstat即可) 这些命令所提供的指标能够帮助你实践USE方法:这是一种用于定位性能瓶颈的方法论。你可以以此检查所有资源(CPU,内存,硬盘,等等)的使用量,是否饱和,以及是否存在错误。同时请留意上一次检查正常的时刻,这将帮助你减少待分析的对象,并指明调查的方向。(译注:USE方法,就是检查每一项资源的使用量(utilization)、饱和(saturation)、错误(error))

接下来的章节里我们将结合实际例子讲解这些命令。如果你想了解更多的相关信息,请查看它们的man page。

1. uptime

$ uptime
 23:51:26 up 21:31,  1 user,  load average: 30.02, 26.43, 19.02

这个命令显示了要运行的任务(进程)数,通过它能够快速了解系统的平均负载。在Linux上,这些数值既包括正在或准备运行在CPU上的进程,也包括阻塞在uninterruptible I/O(通常是磁盘I/O)上的进程。它展示了资源负载(或需求)的大致情况,不过进一步的解读还有待其它工具的协助。对它的具体数值不用太较真。

最右的三个数值分别是1分钟、5分钟、15分钟系统负载的移动平均值。它们共同展现了负载随时间变动的情况。举个例子,假设你被要求去检查一个出了问题的服务器,而它最近1分钟的负载远远低于15分钟的负载,那么你很可能已经扑了个空。

在上面的例子中,负载均值最近呈上升态势,其中1分钟值高达30,而15分钟值仅有19。这种现象有许多种解释,很有可能是对CPU的争用;该系列的第3个和第4个命令——vmstatmpstat——可以帮助我们进一步确定问题所在。

2. dmesg | tail

$ dmesg | tail
[1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0
[...]
[1880957.563400] Out of memory: Kill process 18694 (perl) score 246 or sacrifice child
[1880957.563408] Killed process 18694 (perl) total-vm:1972392kB, anon-rss:1953348kB, file-rss:0kB
[2320864.954447] TCP: Possible SYN flooding on port 7001. Dropping request.  Check SNMP counters.

这个命令显示了最新的10个系统信息,如果有的话。注意会导致性能问题的错误信息。上面的例子里就包括对过多占用内存的某进程的死刑判决,还有丢弃TCP请求的公告。

不要漏了这一步!检查dmesg总是值得的。

3. vmstat 1

$ vmstat 1
procs ---------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
34  0    0 200889792  73708 591828    0    0     0     5    6   10 96  1  3  0  0
32  0    0 200889920  73708 591860    0    0     0   592 13284 4282 98  1  1  0  0
32  0    0 200890112  73708 591860    0    0     0     0 9501 2154 99  1  0  0  0
32  0    0 200889568  73712 591856    0    0     0    48 11900 2459 99  0  0  0  0
32  0    0 200890208  73712 591860    0    0     0     0 15898 4840 98  1  1  0  0
^C

vmstat(8),是“virtual memory stat”的简称,几十年前就已经包括在BSD套件之中,一直以来都是居家常备的工具。它会逐行输出服务器关键数据的统计结果。

通过指定1作为vmstat的输入参数,它会输出每一秒内的统计结果。(在我们当前使用的)vmstat输出的第一行数据是从启动到现在的平均数据,而不是前一秒的数据。所以我们可以跳过第一行,看看后面几行的情况。

检查下面各列:

r:等待CPU的进程数。该指标能更好地判定CPU是否饱和,因为它不包括I/O。简单地说,r值高于CPU数时就意味着饱和。

free:空闲的内存千字节数。如果你数不清有多少位,就说明系统内存是充足的。接下来要讲到的第7个命令,free -m,能够更清楚地说明空闲内存的状态。

si,so:Swap-ins和Swap-outs。如果它们不为零,意味着内存已经不足,开始动用交换空间的存粮了。

us,sy,id,wa,st:它们是所有CPU的使用百分比。它们分别表示user time,system time(处于内核态的时间),idle,wait I/O和steal time(被其它租户,或者是租户自己的Xen隔离设备驱动域(isolated driver domain),所占用的时间)。

通过相加us和sy的百分比,你可以确定CPU是否处于忙碌状态。一个持续不变的wait I/O意味着瓶颈在硬盘上,这种情况往往伴随着CPU的空闲,因为任务都卡在磁盘I/O上了。你可以把wait I/O当作CPU空闲的另一种形式,它额外给出了CPU空闲的线索。

I/O处理同样会消耗系统时间。一个高于20%的平均系统时间,往往值得进一步发掘:也许系统花在I/O的时太长了。

在上面的例子中,CPU基本把时间花在用户态里面,意味着跑在上面的应用占用了大部分时间。此外,CPU平均使用率在90%之上。这不一定是个问题;检查下“r”列,看看是否饱和了。

4. mpstat -P ALL 1

$ mpstat -P ALL 1
Linux 3.13.0-49-generic (titanclusters-xxxxx)  07/14/2015  _x86_64_ (32 CPU)

07:38:49 PM  CPU   %usr  %nice   %sys %iowait   %irq  %soft  %steal  %guest  %gnice  %idle
07:38:50 PM  all  98.47   0.00   0.75    0.00   0.00   0.00    0.00    0.00    0.00   0.78
07:38:50 PM    0  96.04   0.00   2.97    0.00   0.00   0.00    0.00    0.00    0.00   0.99
07:38:50 PM    1  97.00   0.00   1.00    0.00   0.00   0.00    0.00    0.00    0.00   2.00
07:38:50 PM    2  98.00   0.00   1.00    0.00   0.00   0.00    0.00    0.00    0.00   1.00
07:38:50 PM    3  96.97   0.00   0.00    0.00   0.00   0.00    0.00    0.00    0.00   3.03
[...]

这个命令显示每个CPU的时间使用百分比,你可以用它来检查CPU是否存在负载不均衡。单个过于忙碌的CPU可能意味着整个应用只有单个线程在工作。

5. pidstat 1

$ pidstat 1
Linux 3.13.0-49-generic (titanclusters-xxxxx)  07/14/2015    _x86_64_    (32 CPU)

07:41:02 PM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
07:41:03 PM     0         9    0.00    0.94    0.00    0.94     1  rcuos/0
07:41:03 PM     0      4214    5.66    5.66    0.00   11.32    15  mesos-slave
07:41:03 PM     0      4354    0.94    0.94    0.00    1.89     8  java
07:41:03 PM     0      6521 1596.23    1.89    0.00 1598.11    27  java
07:41:03 PM     0      6564 1571.70    7.55    0.00 1579.25    28  java
07:41:03 PM 60004     60154    0.94    4.72    0.00    5.66     9  pidstat

07:41:03 PM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
07:41:04 PM     0      4214    6.00    2.00    0.00    8.00    15  mesos-slave
07:41:04 PM     0      6521 1590.00    1.00    0.00 1591.00    27  java
07:41:04 PM     0      6564 1573.00   10.00    0.00 1583.00    28  java
07:41:04 PM   108      6718    1.00    0.00    0.00    1.00     0  snmp-pass
07:41:04 PM 60004     60154    1.00    4.00    0.00    5.00     9  pidstat
^C

pidstat看上去就像top,不过top的输出会覆盖掉之前的输出,而pidstat的输出则添加在之前的输出的后面。这有利于观察数据随时间的变动情况,也便于把你看到的内容复制粘贴到调查报告中。

上面的例子表明,CPU主要消耗在两个java进程上。%CPU列是在各个CPU上的使用量的总和;1591%意味着java进程消耗了将近16个CPU。

6. iostat -xz 1

$ iostat -xz 1
Linux 3.13.0-49-generic (titanclusters-xxxxx)  07/14/2015  _x86_64_ (32 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          73.96    0.00    3.73    0.03    0.06   22.21

Device:   rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
xvda        0.00     0.23    0.21    0.18     4.52     2.08    34.37     0.00    9.98   13.80    5.42   2.44   0.09
xvdb        0.01     0.00    1.02    8.94   127.97   598.53   145.79     0.00    0.43    1.78    0.28   0.25   0.25
xvdc        0.01     0.00    1.02    8.86   127.79   595.94   146.50     0.00    0.45    1.82    0.30   0.27   0.26
dm-0        0.00     0.00    0.69    2.32    10.47    31.69    28.01     0.01    3.23    0.71    3.98   0.13   0.04
dm-1        0.00     0.00    0.00    0.94     0.01     3.78     8.00     0.33  345.84    0.04  346.81   0.01   0.00
dm-2        0.00     0.00    0.09    0.07     1.35     0.36    22.50     0.00    2.55    0.23    5.62   1.78   0.03
[...]
^C

这个命令可以弄清块设备(磁盘)的状况,包括工作负载和处理性能。注意以下各项:

r/s,w/s,rkB/s,wkB/s:分别表示每秒设备读次数,写次数,读的KB数,写的KB数。它们描述了磁盘的工作负载。也许性能问题就是由过高的负载所造成的。

await:I/O平均时间,以毫秒作单位。它是应用中I/O处理所实际消耗的时间,因为其中既包括排队用时也包括处理用时。如果它比预期的大,就意味着设备饱和了,或者设备出了问题。

avgqu-sz:分配给设备的平均请求数。大于1表示设备已经饱和了。(不过有些设备可以并行处理请求,比如由多个磁盘组成的虚拟设备)

%util:设备使用率。这个值显示了设备每秒内工作时间的百分比,一般都处于高位。低于60%通常是低性能的表现(也可以从await中看出),不过这个得看设备的类型。接近100%通常意味着饱和。

如果某个存储设备是由多个物理磁盘组成的逻辑磁盘设备,100%的使用率可能只是意味着I/O占用

请牢记于心,disk I/O性能低不一定是个问题。应用的I/O往往是异步的(比如预读(read-ahead)和写缓冲(buffering for writes)),所以不一定会被阻塞并遭受延迟。

7. free -m

$ free -m
             total       used       free     shared    buffers     cached
Mem:        245998      24545     221453         83         59        541
-/+ buffers/cache:      23944     222053
Swap:            0          0          0

右边的两列显示:
buffers:用于块设备I/O的缓冲区缓存
cached:用于文件系统的页缓存
它们的值接近于0时,往往导致较高的磁盘I/O(可以通过iostat确认)和糟糕的性能。上面的例子里没有这个问题,每一列都有好几M呢。

比起第一行,-/+ buffers/cache提供的内存使用量会更加准确些。Linux会把暂时用不上的内存用作缓存,一旦应用需要的时候立刻重新分配给它。所以部分被用作缓存的内存其实也算是空闲内存,第二行以此修订了实际的内存使用量。为了解释这一点, 甚至有人专门建了个网站: linuxatemyram

如果你在Linux上安装了ZFS,正如我们在一些服务上所做的,这一点会变得更加迷惑,因为ZFS它自己的文件系统缓存不算入free -m。有时系统看上去已经没有多少空闲内存可用了,其实内存都待在ZFS的缓存里呢。

8. sar -n DEV 1

$ sar -n DEV 1
Linux 3.13.0-49-generic (titanclusters-xxxxx)  07/14/2015     _x86_64_    (32 CPU)

12:16:48 AM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
12:16:49 AM      eth0  18763.00   5032.00  20686.42    478.30      0.00      0.00      0.00      0.00
12:16:49 AM        lo     14.00     14.00      1.36      1.36      0.00      0.00      0.00      0.00
12:16:49 AM   docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00

12:16:49 AM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
12:16:50 AM      eth0  19763.00   5101.00  21999.10    482.56      0.00      0.00      0.00      0.00
12:16:50 AM        lo     20.00     20.00      3.25      3.25      0.00      0.00      0.00      0.00
12:16:50 AM   docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
^C

这个命令可以用于检查网络流量的工作负载:rxkB/s和txkB/s,以及它是否达到限额了。上面的例子中,eth0接收的流量达到22Mbytes/s,也即176Mbits/sec(限额是1Gbit/sec)

我们用的版本中还提供了%ifutil作为设备使用率(接收和发送两者中的最大值)的指标。我们也可以用Brendan的nicstat计量这个值。一如nicstatsar显示的这个值不一定是对的,在这个例子里面就没能正常工作(0.00)。

9. sar -n TCP,ETCP 1

$ sar -n TCP,ETCP 1
Linux 3.13.0-49-generic (titanclusters-xxxxx)  07/14/2015    _x86_64_    (32 CPU)

12:17:19 AM  active/s passive/s    iseg/s    oseg/s
12:17:20 AM      1.00      0.00  10233.00  18846.00

12:17:19 AM  atmptf/s  estres/s retrans/s isegerr/s   orsts/s
12:17:20 AM      0.00      0.00      0.00      0.00      0.00

12:17:20 AM  active/s passive/s    iseg/s    oseg/s
12:17:21 AM      1.00      0.00   8359.00   6039.00

12:17:20 AM  atmptf/s  estres/s retrans/s isegerr/s   orsts/s
12:17:21 AM      0.00      0.00      0.00      0.00      0.00
^C

这个命令显示一些关键TCP指标的汇总。其中包括:
active/s:本地每秒创建的TCP连接数(比如concept()创建的)
passive/s:远程每秒创建的TCP连接数(比如accept()创建的)
retrans/s:每秒TCP重传次数

主动连接数(active)和被动连接数(passive)通常可以用来粗略地描述系统负载。可以认为主动连接是对外的,而被动连接是对内的,虽然严格来说不完全是这个样子。(比如,一个从localhost到localhost的连接)

重传是网络或系统问题的一个信号;它可能是不可靠的网络(比如公网)所造成的,也有可能是服务器已经过载并开始丢包。在上面的例子中,每秒只创建一个新的TCP连接。

10. top

$ top
top - 00:15:40 up 21:56,  1 user,  load average: 31.09, 29.87, 29.92
Tasks: 871 total,   1 running, 868 sleeping,   0 stopped,   2 zombie
%Cpu(s): 96.8 us,  0.4 sy,  0.0 ni,  2.7 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:  25190241+total, 24921688 used, 22698073+free,    60448 buffers
KiB Swap:        0 total,        0 used,        0 free.   554208 cached Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 20248 root      20   0  0.227t 0.012t  18748 S  3090  5.2  29812:58 java
  4213 root      20   0 2722544  64640  44232 S  23.5  0.0 233:35.37 mesos-slave
 66128 titancl+  20   0   24344   2332   1172 R   1.0  0.0   0:00.07 top
  5235 root      20   0 38.227g 547004  49996 S   0.7  0.2   2:02.74 java
  4299 root      20   0 20.015g 2.682g  16836 S   0.3  1.1  33:14.42 java
     1 root      20   0   33620   2920   1496 S   0.0  0.0   0:03.82 init
     2 root      20   0       0      0      0 S   0.0  0.0   0:00.02 kthreadd
     3 root      20   0       0      0      0 S   0.0  0.0   0:05.35 ksoftirqd/0
     5 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H
     6 root      20   0       0      0      0 S   0.0  0.0   0:06.94 kworker/u256:0
     8 root      20   0       0      0      0 S   0.0  0.0   2:38.05 rcu_sched

top命令包括很多我们之前检查过的指标。它适合用来查看相比于之前的命令输出的结果,负载有了哪些变动。

不能清晰显示数据随时间变动的情况,这是top的一个缺点。相较而言,vmstatpidstat的输出不会覆盖掉之前的结果,因此更适合查看数据随时间的变动情况。另外,如果你不能及时暂停top的输出(Ctrl-s暂停,Ctrl-q继续),也许某些关键线索会湮灭在新的输出中。

在这之后...

有很多工具和方法论有助于你深入地发掘问题。Brendan在2015年Velocity大会上的Linux Performance Tools tutorial中列出超过40个命令,覆盖了观测、基准测试、调优、静态性能调优、分析(profile),和追踪(tracing)多个方面。

关于网站加速的35条法则(来自Yahoo)

原文见此:https://developer.yahoo.com/performance/rules.html
注意,不是翻译,只是谈谈本人的读后感。
另外注意,该文比较旧,大概是2010年的产物,所以里面会有些跟不上时代的内容。

1. 减少HTTP请求

一个典型的http请求报文大概是这样的:

GET /3.3.0/build/yui/yui-min.js 
HTTP/1.1Host: yui-s.yahooapis.com
Pragma: no-cache
Cache-Control: no-cache...

虽然也就几行文字,但是考虑到http协议里,对同一个域名同时发出的请求是受限的[1],如果请求太多,说不定它们会堵塞在队列中呢。哦,对了,忘记把cookie算上去了,每次请求的时候都会附带当前域下的cookie,有时候关这一项就有几百B呢。

解决方法:

  1. 文件打包使用CSS Sprites: 将小图标们合并成一幅大背景图,再通过恰当地设置background-imagebackground-position来取出。根据你所用的语言和框架,一般都能找到相关的工具来完成这一任务。除了可以打包图片,还可以打包css文件和js文件。把多个相关js文件和css文件打包成单一的js文件或css文件,省下的http请求数量。这也可以交由工具去做。比如Flask可以使用Flask-Assets
  2. 内联图片可以用data: URL模式来内联图片。比如Github 404页面上的几幅图片:https://github.com/404

2. 使用CDN

据原文的数据,对于最终用户,80-90%的响应时间都消耗在下载页面的各种组件(js、css、flash等等)中。所以加速网站响应时间,就得加速各种静态资源的下载。要想让用户尽快下载到静态资源,根据物理法则,就要把它们放到离用户最近的地方。这时候,CDN就有用武之地了。

什么是CDN

简单说,就是通过用户就近性(IP地址)和服务器负载的判断,CDN会让用户从离他们最近的内容服务器中下载所需的静态资源。据原文数据,Yahoo将静态文件迁徙到CDN之后,响应速度加快了20%以上。当然对于一般厂商而言,不可能在全国各地自建CDN机房,这时就需要购买第三方的CDN服务了。

3. 在响应报文添加Expire和Cache-Control

Expire和Cache-Control的介绍见这里:http://www.path8.net/tn/archives/2745
这是设置浏览器缓存用的。注意,如果你设置了较长时间的缓存,那么每次修改组件内容时,也需要一并修改组件名字,否则浏览器不会重新发起请求。这就是为什么我们看到的许多js和css文件都带着hash戳或者时间戳。

4. 用Gzip压缩组件

什么是Gzip

从HTTP/1.1开始,如果客户端支持压缩,会在在HTTP请求中添加Accept-Encoding: gzip, deflate,服务器就能够据此返回压缩后的数据。(并在响应报文中设定Content-Encoding: gzip)压缩后的数据可以减少多达70%现在就打开你的浏览器的开发者工具,查看响应报文,你会看到你所浏览的网页是经过gzip压缩的。而且毫无解压上的延迟,对吧?你可以gzip一切,除了图片和pdf,因为这些文件一般都是压缩过了的,使用gzip甚至可能会增大文件大小。

5. 把css文件链接放在顶部

不解释,这是html的规范了。

Unlike A, it may only appear in the HEAD section of a document, although it may appear any number of times[2]

6. 把js文件链接放在底部

道理基本上是众人皆知。因为js文件下载后,就会被浏览器执行,同时页面的渲染会被阻塞掉。要知道,等到页面中链接的js文件、js文件中引用的其他js文件都执行完才渲染页面,用户可能已经不耐烦地按下F5了。

你也可以看下script元素的defer属性。

7. 避免使用css表达式

这种东西已经不存在了。

8. 外链js和css文件

第一条规则说到,我们应该尽量减少js和css文件个数来减少HTTP请求,那么减少到多少才是合适呢?取个极端情况,能不能完全把js和css代码内联到html文件中?

减少js和css文件数,需要注意一种情况。假如有些js和css文件经常改变,那么把它们合并在一起,会导致整个文件的改变,以及浏览器缓存的失效。所以更好的做法是,规划静态资源文件,把相对不变的合并在一起,把频繁变易的分隔开。

9. 减少DNS查找

每次访问互联网上的一个主机,假如没有命中DNS缓存,就会发起一次DNS查询,会消耗掉一定时间。如果把资源文件分布在不同的主机中,就会增加DNS缓存不命中次数,不过这又跟前面的几天规则相违背,所以还是需要权衡啊。当然如果贵司足够霸气,可以考虑下面这个方案:全局精确流量调度新思路-HttpDNS服务详解

10. 压缩js和css文件

不解释。

11. 避免重定向

该用的时候还是得用。

12. 移除重复的script

13. 配置ETag

后端可以在响应报文中添加Etag这一项,那么当浏览器下次请求同样的资源时,会携带If-None-Match条目。假如Etag没有发生变化,服务器可以返回304 Not Modified状态码,无须重新下载资源。

例如:

响应报文:

  HTTP/1.1 200 OK       
  Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT       
  ETag: "10c24bc-4ab-457e1c1f"       
  Content-Length: 12195

下一次的请求报文 :

  GET /i/yahoo.gif HTTP/1.1       
  Host: us.yimg.com       
  If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT       
  If-None-Match: "10c24bc-4ab-457e1c1f"       
  HTTP/1.1 304 Not Modified

这件事一般交由你用的服务器(比如Nginx)去做

14. 缓存Ajax

使用前面讲到的技术,如expire、cache-control、etag等,让浏览器将Ajax返回的结果也缓存起来。

15. 提前返回后端生成的内容

在后端的html页面完全渲染完之前,先返回部分内容。文中以PHP作为一个例子:

      ...<!-- css, js -->     
      </head>    
      <?php flush(); ?>     
      <body>       
      ... <!-- content -->

调用php的flush函数来“冲刷”后端缓冲区中的内容。貌似是先返回head中的内容,让浏览器加载css文件,具体得由懂php的人来解释下……
这个要取决于你用的框架和语言是否允许这么做

16. 在Ajax请求中使用GET方法

因为在浏览器的实现中,GET方法耗时更短。
不过这得看业务逻辑的,对不对?

17. 延迟加载

如果可能,直到需要用时才加载某些图片、js文件……这方面有很多可用的库,比如echo.js

18. 预先加载

呃,看待事物果然不能太绝对……这里的预先加载,是说在浏览器空闲的时候,预先加载用户接下来要浏览的内容。文中举了Google首页中的css sprite作为例子。虽然首页用不上这个sprite,但是考虑到用户基本不会停留在Google首页,而且首页内容较少,于是预先加载了这个sprite

19. 减少DOM元素数量

过多的DOM元素会拖慢js执行的数目。特别是有些页面,使用div层层嵌套。原文强调尽量少用额外添加div的方式来实现某种效果,要考虑到html的语义。

运行document.getElementsByTagName('*').length看下当前页面有多少个DOM元素,跟优秀的同类页面比较下。

20. 将不同的内容分到不同的域名下

理由见规则1
同时注意规则9的影响,不要分得太多。

21. 最小化iframe的数目

<iframe> 好处都有啥: - 加载第三方内容 - 作为沙盒 - 并行下载脚本 - 加载那些通用的内容(但是又不打算改为单页应用) <iframe> 的坏处呢: - 花销 - 阻塞页面加载 - 语义丢失 ## 22. 减少404

对静态资源的请求,避免返回404

23. 减少cookie大小

消除无用的cookie
尽量最小化cookie的大小
恰当地设置cookie的作用域,以免影响其他子域名
恰当地设置cookie的过期时间(如果不设置的话,一旦浏览器关闭,cookie就会失效。所以无关紧要的cookie就不要设置过期时间了)

24. 给静态资源分配一个无cookie的域名

一般情况下,请求静态资源是不需要带cookie的,所以把它们独立开来。参见规则1

25. 减少js对DOM的访问Minimize DOM Access

三种方法:

  1. 缓存DOM元素的引用
  2. 批处理对DOM的修改,而不是每次都调用DOM方法(stackoverflow上有一个相关的回答:http://stackoverflow.com/questions/14291811/minimize-dom-access-inorder-to-have-a-more-responsive-page)
  3. 避免使用js来解决布局上的问题

26. 更明智地使用事件监听器

假如你在一个div下有十个按钮,可以只在那个div上添加事件监听(再通过Event参数分清来源),因为事件会冒泡的。
又比如监听DOMContentLoaded事件而不是Load事件。

27. 使用 而不是 @import

这篇文章:
高性能网站设计:不要使用@import
已经交代了一切。
又可以黑IE了。

28. 避免用AlphaImageLoader

Yet another历史问题。现在无须担心这个了。

29. 优化图片

其实除了在js和css文件上动刀子,我们也可以优化图片。
文中提到了用PNG代替GIF(所以说里面的内容已经有点老了)
还推荐一些工具,如imagepicker、pngcrush、jpegtran。
总之,去除多余的图像信息,如果允许,可以牺牲下图片质量。

30. 优化CSS Sprites

将小图片水平排放而不是竖直排放。
减少图片间的间隔。
使用上述的优化图片的方式。

31. 不要在HTML中设置图片

不要使用过大的图片,然后在HTML里给它设置一个合适的(更小的)尺寸。

32. 让favicon.ico小而易于缓存

favicon.ico应该小于1k。可以考虑给它设置一个Expire报头,如果你有打算修改它的话,毕竟你不能改变它的命名。

33. 让组件小于25K

之所以要小于25K这个Magic Number,是因为iPhone不会缓存大于25K的组件。意味着如果有的文件大于25K,每次访问时都需要重新获取。

不过考虑到本文内容较老,我还是搜索求证下,最后找到了这个网址:
http://www.slideshare.net/cafenoirdesign/the-future-is-mobile-11719438 (需梯子)

Double image dimensions, then resize✤ Individual component caching: iOS 3.x will only cache HTML pages under 25k , iOS 4 102.4 kb per item✤ Total component caching: Android and iOS 4 set limit at 2MB✤ gzip has no effect on cache-ability on any device

简单说,25K限制是iOS 3时代的产物,对于现在的移动端,基本上不需要担心组件太大而无法缓存的问题(不过用户会比你更在意流量耗费的问题)。

34. 将组件打包成multipart类型

指定Content-Type为multipart,然后在一个响应报文中发布多段数据……呃,基本上没什么会用吧,太过于复杂而且效果不显著。

35. 避免空的src属性

这种情况有两种版本:

<!-- html -->
<img src="">
// javascript
var img = new Image();
img.src = "";

都会导致额外的、徒劳无功的请求。

同理,以下代码也有同样的问题:

<script src="">
<link href="">

不过在HTML5中规定,只有合法的URL引用才会产生新的请求,所以如今类似这样的空属性不应该会带来额外的负担。

高效人士的16个习惯

标题是借鉴于某鸡汤……原文在此:https://zapier.com/blog/best-ways-work-smarter-not-harder/

不是翻译,只是写个读后感。

1. 停止多任务

多任务使人产生一种“我今天做了很多事”的幻觉,正如多任务计算机使用户产生自己独占了整个计算资源的幻觉一样。

今天,大部分的操作系统都是支持多任务的,这让人以为多任务是一种先进的特性,支持多任务的操作系统能够在单位时间中完成更多的工作。实际上,支持多任务的操作系统,效率并不比不支持多任务的操作系统要高。之所以要支持多任务,是因为它们需要跟人交互,而只有能够在任务间流畅切换,才能及时响应人的各种指令。

据说,由于任务间切换需要花费大量的时间和空间进行上下文切换,支持多任务的操作系统需要付出4%的性能。这个数字可能不准确,不过多任务的确降低了操作系统的效率,而非提高。人也一样。

2. 多休息

90分钟的工作配15到20分钟的休息……呃,这个因人而异吧。我就是经常坐不定O(∩_∩)O。
要想多休息,建议把水杯放远点,这样就能多休息了。(或者使用编译型语言,参照那个xkcd的老梗

3. 提前做一周计划

4. 批处理同类任务

这一条正好是跟第一条相对照的,批处理同类任务减少了在不同任务间切换的开销。

5. 根据你的精力调度任务

计划安排要考虑自己的体质特性。或许可以像记录生理周期一样记录自己在一天中通常的精力变化。

6. 裁剪to-do表

保持小步快走

7. 下午打个盹

8. 关掉各提醒

现在开始闭关吧!

9. 播种番茄钟

文中提到了一个“惩罚性的番茄钟”,如果你不能在25分钟内坚持一件事,那么就在本子中某一页打上一个tick……这么一来,你会出于愧疚感而不得不坚持。

10. 拿起纸和笔

返璞归真。纸和笔会将你的注意力从各种“效率”app中拯救出来。

11. 时时自省之

总结每一天是怎么过的,反思自己的计划和达成。这样就会有满满的羞愧感刺激你奋发图强可以不断地改进自己的习惯,抛弃错误的做法,一点一滴地提高效率。

12. 尽力自动化

这个是程序员的特长!开动吧,我的程序们!工作就交给你们了,主人还有许多要务要忙呢!

13. 拥抱大自然

不要再把自己囚在陋室之中啦!

14. 早起

这个可做不到!

15. 为工作开启背景音乐

16. 设置一个启动时间

Get it now!

chrome devtool 技巧 之 开篇

作为一个chrome用户 && 写前端的程式设计师, chrome开发者工具(chrome devtool)基本上几乎是每天都用到。

虽然之前已经掌握了devtool的基本用法,比如查看元素和设置js断点,在console中测试js代码之类。不过这些只是一些基本的技巧,只是devtool中常用的20%功能,还有80%的高级功能没有用到呢。而且,自身也没有系统去学习。

所以现在就打算挤出几天时间,把官方的文档浏览一遍,系统地学习下devtool的使用技巧。

这里是第一篇,开篇,陈述了我写下这个系列的由头,还有一些不适合放在独立的篇章的内容,比如下面的一些快捷键

一些快捷键

<C-F> : 在所有的文件中搜索
<C-o> : 快速打开某个文件

推荐一篇必看的文章

https://developer.chrome.com/devtools/docs/authoring-development-workflow

虽然未能覆盖devtool许多功能,而且某些我觉得重要的功能这里也没提,这篇文章还是相当值得一读。它覆盖了许多devtool使用中的要点。

浅谈jmockit中mock机制的实现

最近在工作中写单元测试的时候,用到jmockit来mock无关对象。

在jmockit中,你可以使用MockUp来创建一个“fake”的实例,对某个方法指定自己的实现,而不是调用实际的方法。

对于接口类型,需要这样调用:

@Mocked
private SomeInterface mockInstance;

mockInstance = new MockUp<SomeInteface>() {
    ...
}.getMockInstance();

这个倒没有什么古怪的。估计又是使用了java.reflect.Proxy。这个技巧在很多Java框架中用到,比如Spring AOP对于接口类型的实现,就是通过Proxy来混入拦截器实现的。

但是,对于其他类型的调用,就比较奇怪了:

@Mocked
private SomeProxy mockInstance;

new MockUp<SomeProxy>() {
    @Mock
    public int doSth() {
        return 1;
    }
};

mockInstance.doSth(); // return 1

new出来的对象,如果没有赋值给新的变量,应该是随着GC风飘云散了。可就是在我的眼皮底下,mockInstance就这样被掉包了。

Spring AOP中,对于非接口类型,是通过CGLIB魔改字节码来实现拦截器注入的。所以我估计这个也是一样的道理。不过令人想不通的是,jmockit到底是什么时候进行移花接木的?没看到注入的地方啊……

只能通过看代码来揭秘了。先从MockUp的构造函数开始吧。

// MockUp.java
@Nonnull
private Class<T> redefineClassOrImplementInterface(@Nonnull Class<T> classToMock)
{
  if (classToMock.isInterface()) {
     return createInstanceOfMockedImplementationClass(classToMock, mockedType);
  }

  Class<T> realClass = classToMock;

  if (isAbstract(classToMock.getModifiers())) {
     classToMock = new ConcreteSubclass<T>(classToMock).generateClass();
  }

  classesToRestore = redefineMethods(realClass, classToMock, mockedType);
  return classToMock;
}

对于非接口类型,调用了redefineMethods来定义一个仿版。顺着redefineMethods找下去,终于发现了jmockit的“作案手法”。

// MockClassSetup.java
@Nullable
private byte[] modifyRealClass(@Nonnull Class<?> classToModify)
{
  if (rcReader == null) {
     rcReader = createClassReaderForRealClass(classToModify);
  }

  MockupsModifier modifier = new MockupsModifier(rcReader, classToModify, mockUp, mockMethods);
  rcReader.accept(modifier, SKIP_FRAMES);

  return modifier.wasModified() ? modifier.toByteArray() : null;
}

看到byte[]的函数返回类型,估计就是在这里实现了字节码的转换,然后返回了新的被掉包的class文件了。沿着MockupsModifier看下去,可以看到jmockit是用ASM来改动原来的实现(具体见external.asm这个包,我就没有细看了)。

众所周知,Java代码先是编译成class文件,然后由JVM加载运行的。围绕JVM这一中间层,各种有趣的技术应运而生。比如各种类加载器,可以动态地去加载同名的类的不同实现(不同的class文件)。还有各种魔改class文件的手段,在原来的实现中注入自己的代码,像ASM、javassist、GCLIB,等等。jmockit就是应用ASM来修改原来的class文件,用mocked的实现掉包原来的代码。因为MockUp的构造已经触发了“狸猫换太子”的幕后行为,所以这里就不用把new出来的东西赋值给具体变量了。

还有一个问题。我们虽然弄明白了jmockit的作案手法,可是还没有找到掉包现场呢!即使现在jmockit已经持有了被篡改后的字节码,可它又是怎么替换呢?

继续看下去,发现jmockit把修改后的字节码存在StartUp.java里面了。转过去会看到,jmockit这里用到了JDK6的一个新特性:动态Instrumentation。怪不得jmockit要求JDK版本知识在6以上。

关于动态Instrumentation,具体可以看下这篇文章:http://www.ibm.com/developerworks/cn/java/j-lo-jse61/
简单来说,通过这一机制可以实现监听JVM加载类的事件,并在此之前运行自己的挂钩方法。这么一来,掉包现场也找到了。

那jmockit怎么知道要监听哪些类呢?前面可以看到,需要Mock的类上,要添加Mocked注解。所以jmockit编写了一些跟主流测试框架集成的代码,在测试运行的时候获取带该注解的类。这样就知道要监听的目标了。

总结一下:jmockit先通过Mocked注解标记需要Mock掉的类。然后调用new MockUp去创建修改后的class文件。在JVM运行的时候,通过JDK6之后的动态Instrumentation特性监听类加载事件,并在目标类加载之前移花接木,用魔改后的字节码换掉真货。虽然Java是门静态类型语言,不过幸亏有字节码和JVM作为中间层,使得mock实现起来相对容易。

chrome devtool 技巧 之 console

console面板是devtool中使用最多的三个面板之一(另外两个分别是Elements和Sources)。
这一章来介绍下console中的小技巧 😄

有一个快捷键可以直接打开console面板:<C-J>

js上下文

console中运行的函数位于当前js上下文中。什么是js上下文呢?就是假如你现在正处于某个断点,这个断点所在的上下文中有一个变量a,那么在console中你也可以获取这个变量的值。

当然还可以运行上下文中的函数,包括jQuery函数和Underscore函数。以我的看法,插件中的js应该是跟页面共享一个上下文环境的。不过就算你用的插件中使用了jQuery,如果当前载入的页面没有用到jQuery,你还是没办法使用jQuery的。

command line api

下面列出些我觉得比较有用的命令行api。这些api可以直接在console面板中使用。

$()
等价于document.querySelector()
$$()
等价于document.querySelectorAll()

monitor(function)
监控给定函数的调用情况。当函数调用时输出函数调用提示和调用参数。使用**unmonitor(function)**来取消监控。

monitorEvent(object, [events])
监控发生在给定对象上的事件。给定对象可以是window,也可以是$('.some')这样的DOM对象。
给定的事件可以是单个事件,也可以是['click', 'hover']这样的列表。
简直就是神器。美中不足的是不支持mouseentermouseleave,不能监控hover事件了。
详细的事件支持列表见下:
https://developer.chrome.com/devtools/docs/commandline-api#monitoreventsobject-events
同样是用**unmonitorEvent(object, [events])**解除绑定。

debug(function)
给定函数调用时,自动切换到Source面板开始debug工作。
使用**undebug(function)**解除

console.time(tag)
console.timeEnd(tag)
浏览器的新功能:精确到ms的秒表!用来记录方便面的浸泡时间似乎有点大材小用呢

profile(tag)
profileEnd(tag)
开启/关闭CPU分析。分析结果需要切换到profiles面板中查看。结果文件名为tag名。
可以看到一段时间内调用的函数(虽然大部分是匿名函数……)

右键菜单

最后谈谈在console中可用的右键菜单。

  • 右键菜单,选择Log XMLHttpRequests,就可以记录下所有的xhr,方便调试Ajax。
  • 在console面板中显示出来的DOM对象上右键,会看到reveal in Elements panel选项,能跳转到Elements面板对应的位置。

chrome devtool 技巧 之 debug

这是chrome devtool技巧的最后一篇了……写完这篇算是填了坑。说好的花几天看看呢,其实花了整整半个月了。

这一篇讲的是devtool中的debug技巧。

debug技巧

debug之道,在于运气,在于淡定;debug之术,在于断点触发,在于定格上下文。

debug之道,道可道,非常道,名可名,非常名。本人吹水水平不高,所以不可能从道的角度上指点江山,只能在具体的术上激扬文字,呃不,扯上几句。

何为断点触发?chrome devtool中提供了普通断点,条件断点,DOM断点和XHR断点这四种断点,基本上无论JS中要执行什么代码,都会有合适的断点用得上。(除非绑定了外面的C++ add-on,不过一般都不会遇到这种问题)

何为定格上下文?当断点被触发时,你就会停下来。这时候会出于一个上下文环境中,在右侧的框中能够查看当前所有的变量,还可以在call stack中查看前面的函数调用。甚至可以在下面的console面板(你可以把它拖上来)中执行各种上下文可用的函数(如果引入了underscore或jQuery,就能直接执行underscore之类的js库的函数哦)。

下面详细介绍下这两个方面。

断点触发

条件断点:正常断点 》 右键菜单 》 Edit breakpoint,添加表达式,当表达式为true时使断点生效。条件断点的颜色为橙色

异常断点:右边最顶上一栏,最后一个像插座的按钮,可以选择是否在(未捕获)异常发生时触发断点

DOM断点:前面关于Element面板那一章说了。这里可以查看都设置了哪些DOM断点

XHR断点:当出现URL匹配的请求时,触发断点。URL模式为空时,匹配所有请求

定格上下文

定格上下文所需的,要找到对应的stack。在chrome中,我们甚至可以让devtool显示之前异步的调用(XHR或timeout事件触发),只需勾选右边的Async选项即可。

有趣的是,在call stack中,如果选择不同的stack,console的上下文也会跟着改变。可以试一下在不同的stack中打印this的值,你会发现不同的stack输出结果会不一样。这意味着,call stack中不仅可以反应当前上下文,还可以切换过往的上下文。 是不是有点时光机的感觉?

我有特殊的debug技巧

待未捕获的异常发生时,通过window的error事件获取报告:

window.onerror = function(msg, url, line){
   console.log('get uncaught error: " + msg + ", url = " + url + ", line = " + line ');
}


另外,在调试的过程中,当程序因为某个断点而停下来时,你甚至可以修改调试中的JS代码(记得修改后需要Ctrl+s保存),从而在等下执行时得到不同的结果。

大概就是这样子。

Clojure初体验

计划在大学前三年学一门函数式语言,目前决定就是学Clojure了。
在昨天晚上踢出临门一脚后,我开始配置Clojure环境。
嗯,作为一个选择困难户,我总共试了三种不同的Clojure环境。

  1. vim

    作为一个vimer,一向首先用vim来尝尝新的语言,这次也不例外。配置我是按照getting-started-with-clojure-in-vim来的,就是在原来长长的插件列表下增加了fireplace.vim和vim-sexp-mappings-for-regular-people这两个插件。不过不知为何,fireplace.vim不能工作……

  2. clockwise - eclipse

    然后我开始折腾clockwise这个eclipse插件。clockwise果然不出意料,第一次打开就crash了。。。呃,这是eclipse的老毛病了。eclipse上什么语言的插件都有,但是除了Java,其他语言的插件都只能拿来显摆,呵呵。即使不crash(其实也不是经常crash),clockwise提供的功能vim也可以做到,clockwise提供不了的功能vim也可以做到。

  3. lighttable

    据说,许多人是通过这个来入门clojure的。上了他们官网看看,外观挺不错的。然后下了个Linux64位版本,结果提示libudev.so.0没找到。而这个库只在32位系统上有……
    于是试着apt-get下载,但是提示说已经被废弃了。所以只能手动下载。然后我通过上游debian的仓库下了对应的libudev.so.0,最后终于成功跑起来了。
    看了下,感觉跟vim差不多,当然好处是不会像eclipse一样经常卡住。反正我最后选择了vim

插一句,那个fireplace不能用的问题,上网搜索一下解决了。

对Clojure的第一印象

括号好多。

前缀表示可以更省字符,而且提高表达力。

不同的form连接在一起,就像一棵树,或者专业一点,称之为S表达式。每个子节点都能求出一个值,而上一级的操作符可以对每个子节点的值进行计算。有点像reduce操作。这样下来就不用写许多本地变量了。不过程序员需要在头脑中处理这些子节点的值,感觉有点考验能力。

语法糖目前还没接触到,目前的看法是,没有语法糖的情况下,Clojure不见得比Ruby或Python更有表达力,反而啰啰嗦嗦,充斥着种种大括号……

另外启动速度比较慢,就跟编译类语言一样(其实Clojure算是编译类型的语言,需要编译成字节码然后启动JVM运行),所以不适合做脚本。不过结合fireplace.vim来用,倒是可以很快看出实验结果。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.