黑客马拉松
软件开发听上去高大上,但实际很简单,全部活动可以分为两类:造轮子,搭积木。这和建筑行业很相似 —— 甚至相似到软件业懒得自己编词,借用建筑业的architect这样的title为自己所用。我的理解「造轮子」就是做一些基础性的工作,如os,compiler,database,protocol(如tcp/ip),algorithm(如DH,RSA),framework(如rails)等,「搭积木」则更多是应用性的工作,利用手边的组件和工具,做出新的产品和工具。「造轮子」需要的知识和能力一般而言要高于「搭积木」(也不尽然),所以大拿们总会说:"don't reinvent the wheels"。不过,技术总是在不断进步,总得有人「造轮子」,所以隔些时候基础领域就会有新思想新公司横空出世。比如PageRank,BigTable和它背后的google,电动汽车的电池技术/电动机技术和它背后的Tesla。
黑客马拉松(Hackathon,Hacker + Marathon)就是一个鼓励「搭积木」而非「造轮子」的活动。你可以在固定的时间(现在多为一天)内随心所欲,去做自己想做的产品。固定的时间就限定了你能做的事情 —— 「造轮子」这种基础性的工作,一天下来可能想还没想清楚呢就过去了,所以最终比拼的是「搭积木」的能力 —— 谁搭出来了,谁搭得漂亮。
Juniper内部的hackathon起源于一年前,现在已经是第三届了。老板们知道「老」程序员们(Juniper员工年龄都偏大)熬夜编程是个挑战,于是慷慨地给了一天开发时间,半天准备presentation的时间,和半天的pitch时间。今天我就结合自己这次hackathon的经历,讲讲在hackathon中如何选题,如何利用好一天的时间鏖战(包括pivot),以及如何pitch。
选题
既然是随心所欲,选题就要选自己想做的东西。如果能往公司的产品上靠(内部hackathon),或者往大会的主旨上靠(外部hackathon)最好;如果不能,也没关系。hackathon的精神是交流学习及努力交付。如果你找不到什么题材,又想参加hackathon,不妨往这几个方向去想:
找到你想做的点之后,要把产品的范围缩小缩小再缩小。千万不要高估你一天之中能做出来的东西。如果你聚精会神毫无打扰地写一天能写1000行代码,那hackathon你的代码量要控制在500行内。如果一个team好几个人做开发,假设三人,那么总代码量最好乘以一个减去沟通成本的有效编码的系数,如0.7,也就是三个人一起写,总代码量控制在1000行。这里举的数字都是估值,具体要看你自己对自己和团队的估计。
产品的范围集中在一个小点上的好处是容易交付。没有什么比做了一天没有任何可交付的软件让人痛心疾首的了。如果很不幸你低估了自己的能力,很快就做出来可交付的产品,那么空闲的时间可以继续完善产品(而不是扩大产品的范畴),让核心功能更突出。
这次hackathon我的选题和visualization相关。我就说说其中和Juniper产品无关的部分吧。在过去的数年里,我们渐渐知道data visualization的巨大威力 —— 下图是当本拉登被击毙后,twitter上的传播路径 —— 复杂的数据流动,通过一张简单的图就说得无比清楚:
其实类似的路径在代码中也存在。一个公司的代码库里面有数百万,数千万行代码,有谁能把它捋清楚,有谁能在很短的时间内了解其中的细节呢?可不可以将代码库可视化,让其能够自己告诉你其中的各种逻辑/调用关系?
这就是我想尝试的。
开工
开工首要解决的问题就是原料,也就是寻找可以用来「搭积木」的「轮子」。在hackathon前备好料最好不过,当天备料也未尝不可。
备料是个很重要的过程,你使用的「轮子」决定了你「搭积木」的高度。很多时候一个产品都少不了和数据打交道,如果使用django的admin(加south),处理数据,存储数据,撰写基础数据都易如反掌 —— 而且你只需要寥寥数行代码(不包括model的代码)就能得到大部分功能。别人也许还在吭哧吭哧地做CRUD,这厢你已经开始聚焦你要解决的核心问题了。
做hackathon经常遇见的问题是做了半天发现手头的产品碍于主观或客观原因无法继续下去,走进了死胡同。这时要果断pivot —— 注意不是另起炉灶。pivot这词不好翻译成中文,借用Tao神的形象解释(如果你看了这几期『途客们地旅行梦』,你应该对他有点印象)—— pivot在篮球场上就是指一只脚作为轴,另一只脚来回探索寻找投篮或传球的机会。当产品进入死胡同,不要立即否定,退后一点做做其它方向上的尝试,也许能找到突破口。记住努力交付是hackathon的精神之一,pivot的目的就是为了交付。
回到我的项目。我们知道,对代码进行profile分为静态和动态两种。我想达到的目标是能够可视化代码中运行时的路径(这个足够cool!)。比如说对linux kernel的代码做研究(举个例子,我真正做的不是这个),我想知道一个个数据包在kernel里走过的全部流程,然后以此绘制热点图,Petri Net等等。
对这个需求点,我需要的「轮子」有:
(1) systemtap,一个linux下媲美DTrace的probing工具。systemtap可以生成kernel module将你需要的代码注入到被监测的点(通过TRAP),达到不改变已有系统的代码,runtime获取信息的效果(你可以把它想象成病毒 - 获取程序的控制权,执行病毒代码,然后恢复原有程序的执行)。
(2) jointjs(DavidDurman/joint)。获得了运行时的数据后,我需要将其可视化,而jointjs就是我找到的一个最称手的工具(但不完美)。我本来是想用d3.js或者sigma.js来做这事的,但前者太基础,需要写很多代码,后者表述的图形种类太少,所以我最终选定了jointjs。
(3) websocketd。这是一个可以把stdout输出转化为websocket数据传递给browser进行展示的好工具。比如下图是我做得一个对vmstat的可视化(实时变化的):
找齐了「轮子」后,我打算这么「搭积木」:使用websocketd来运行systemtap抓取数据(systemtap把数据输出到stdout,websocketd将其发到浏览器),然后前端生成jointjs的绘图代码,生成实施的数据流图。代码量估计不会很大,就几百行的样子(systemtap脚本+javascript)。
可惜人算不如天算,我没斗得过Murphy's Law(凡事可能发生,就必然发生),我工作的几台vm都罢工了,要么业务跑不通,要么vm本身无法访问。下面的systemtap脚本运行也出了不少问题(示例而已,已经大幅裁剪和移除和工作相关的内容):
global ignored global start probe begin { printf ("Start probling...\n") start = 0 } probe process("mydaemon").function("*").call { if (!start) { ignored[probefunc()] = 1 } else { if ([probefunc()] in ignored) { // do nothing } else { printf ("%s -> %s\n", thread_indent(1), probefunc()) } } } probe process("mydaemon").function("*").return { if (start) { if ([probefunc()] in ignored) { } else { printf ("%s <- %s\n", thread_indent(-1), probefunc()) } } } probe timer.s(5) { start = 1 printf("Function call probing started.\n") } probe end { printf ("probing stopped.\n") }
这个脚本监控某个process,任其运行5s,把所有遇到的function call都存入ignored中,5s之后的函数调用关系会被打印出来。之所以选择5s,是想屏蔽系统的噪音 - 初始化代码,各种scheduling,time event代码,打印真正的业务。由于我工作的vm不稳定,业务跑不通,所以无法抓到有效的数据。
整整试了一个早上(大概8:00-11:30),最终我放弃和系统抗争。动态profile走不通,我决定pivot到不那么sexy的静态profile。静态profile就是生成代码的调用关系 —— 我最初打算做一个python脚本,用户输入一个函数名,我为她生成两张图,一张是往前回溯,展示所有调用这个函数的完整路径;另一张是往后追溯,展示所有以它为根的整个调用路径。初见成效后,我便用django将生成的结果管理起来了。
要获得整个代码库的调用关系可以写yacc/lex进行语法分析,但单单做这一件事就要耗去不止一天时间。我想了很多其它方法,也在stackoverflow上到处寻找帮助,但都无疾而终。人在紧急情况下会产生「急智」,就好像突然开窍一般,我突然想到了用cscope生成的索引文件cscope.out。非c/c++,或者非unix平台下工作的程序员可能不知道cscope —— 其实只要你使用IDE,IDE就会生成代码库的索引,跟cscope原理基本一样。我开始打算写个python脚本分析cscope.out里的内容,去寻找函数间的关系,但我没能在cscope的官网上找到相关的文档告诉我该怎么做,也没有找到操作它的API。退而求其次,我只好求助于cscope命令本身。使用过cscope的人大多是用vim/emacs或者直接"cscope -d"使用,我想99%的人不知道cscope还能这么用:
$ cscope -d -L2 <function-name> # print all callee symbols to stdout $ cscope -d -L3 <function-name> # print all caller symbols to stdout
这下我只需要写个python脚本分析这两个命令的输出就OK了。通过分析输出并不断递归这个过程直到stdout没有输出(到达根节点,没有调用关系了),我就能获取某个函数的整个调用关系。当然,这么写代码是权宜之计(workaround),效率很低(因为不断地和cscope进程交互),所以真正高效的做法是找到合适的api,或者自己写yacc/lex。
使用cscope.out的另外一个好处是对代码的分析可以脱离代码本身,任何一个装有cscope的环境就可以进行分析。
静态profile我需要的其它「轮子」还包括graphviz(绘图),django(存储所生成的graph,提供web访问方式)。其中一个function的callee可视化后是这个样子(不用费心啦,你看不清我长什么样子滴^_^):
挺吓人的吧 - 这原图三十多兆,以我高大上的15" new mbp还绘制了半个小时。最终这些图表能够在django做的website中展示出来,还能查询。
前天晚上我写了个多线程的脚本,8个核打满,一晚上直到把我的mbp电池耗尽才绘制了400多个函数的caller graph和callee graph。绘图的效率太低,这performance也就顶多给hackathon做个演示。昨天我本来想修改一下代码,先将中间结果保存在图形数据库neo4j里面(使用neo4django),然后再考虑绘图的事(或直接用jointjs展示到前端),可惜时间不够就放弃了,我还需要写slides做pitch呢。
pitch
pitch是整个hackathon中最耀眼的环节,但又是最不好玩的环节。产品最终是要被展示出来的,这是我喜欢pitch的地方;但pitch的功利性太强,反而冲淡了hack一天带来的愉悦感。
一位做产品的前辈曾跟我说:「做出来的产品要么很受欢迎(大把粉丝),要么有很多争议(很多仇家)。不管怎样,千万不要做出来后大家觉得无关紧要」。我虽然对此论调持保留意见,但我觉得这话用在pitch上比较对路。不管怎么说,一个大家漠不关心的pitch不是好pitch。到了这一步,idea不酷不重要,demo不丰满不重要,重要的是pitch要够吸引人。这很悲哀,但也很现实 —— 和忽悠投资人是一个道理。
所以hackathon最后要多花些时间打磨pitch。我参加过的在北京举行的techrunch hackathon最后team就败在了pitch环节 —— 我们头脑发热做了个情景舞台剧模拟用户的使用场景 —— 天知道当时大家是怎么想的。所以尽管你做出了很cool的产品,如果pitch不好,吸引力还是会大打折扣的。
我现在做pitch除非公司强制,否则绝对不会用powerpoint/keynote —— 并不是这两个工具不好,而是它们已经跟不上互联网的思维了 —— 演示文档要随时随地可达,并且可以嵌入任何内容。我自己一般使用reveal.js和impress.js。当然不是直接用,而是使用我自己结合wintersmith做的一套生成工具。这样我可以用markdown,jade,yml等格式撰写slides,然后编译成html,在chrome里播放。这次pitch我使用的是impress.js。类prezi的炫丽展示效果加上里面引用data visualization的一些高大上的图(包括ben laden的这张开山鼻祖图),足够吸引观众的眼球。
结语
其实参加hackathon最有意思的事情是挑战自己的能力极限,逼出自己的潜能 —— 这是hackathon向marathon致敬的地方。我个人感觉重要的不是受到赞许或者别人注目的快乐,冲线那一刻的快乐,获奖的快乐等等,这些快乐都无法持续很久,也许第二天起床后你就忘记了;真正重要的是沉浸于其中,专注地围绕着一个目标不断学习不断改进,最终交付的整个过程中获得的快乐。我在参加这次hackathon之前从未用过 graphviz(pygraphviz),jointjs,neo4django等等工具,都是现看文档现学习,很受用,很享受learning by doing的快乐。
题图是baltimore hackathon的宣传海报,我觉得它很好地反应了hackathon的精髓。