CruiseControl.rb初体验

我所在的项目一直以C语言作为主要开发语言,与做Java以及其他新兴语言的人不同,组内的同事似乎对新鲜的东西不是那么感兴趣,也没有主动去研究新鲜事物的意愿和意识。我深为此闹心,看到外面世界中那么多美妙的工具,再也不能坐以待毙了。我一直都是有很多想法的,但是迫于自身精力有限,自己无法全身投入,以前都是交予别人去做的,但是收到的效果都不是很好。认识到这点后,我决定自己动手,丰衣足食。

从心底一直对公司的CMMI流程有所抵触,眼看着外面世界中的Agile Development等轻量级开发过程日益壮大,但自己每天还不得不按照各种繁复的流程去做,真有一种"身在曹营心在汉"的感觉。但是在一个CMMI流程"森严"的公司,又如何才能让大家接受Agile的思想呢?我想让大家看到新实践的与以往不同的成效,大家自然也就能够接受了。在Infoq上,有人也给出了建议:“切莫开口提大名词(例如SCRUM或者XP)。建议以CMMI x级的"自我改进"做旗帜,找到组织中存在浪费的环节,引入最佳实践来消除浪费,没有必要把敏捷挂在嘴边 ”。而持续集成也是文章中建议的首选实践,与我的想法不谋而合。

持续集成,以前不是没有做过,在部门的一个项目组中曾经尝试过,自己编写脚本,完成定时更新代码、构建的任务,后因项目紧张,似乎没有收到良好效果。我也曾经在项目组内推进过做Java的同事进行持续集成的实践,并安排一个同事搭建了CruiseControl的服务器,但后来似乎不了了之,也怪我没有在后期给予充分的重视和压力。有过这些教训后,我时常也在想?推进一个持续集成的实践就这么难么?这回我亲自来做,从C开发人员入手。

持续集成是需要良好的工具支持的,业内最知名的持续集成工具莫过于CruiseControl了,CruiseControl在Java开发领域占据着No.1的地位,而且其设计思想也影响着后续的持续集成工具的开发。但是一想到Java,我第一感觉就是复杂,CruiseControl会不会也像配置一个Web服务器一样那样复杂呢?另外CruiseControl使用Maven + Ant + cvs/subversion的组合,我曾经研究过Ant, Maven,大量的xml配置让我感到很头疼,另外是否与C/C++良好配合也是疑问。这样一来,我就没有继续走CruiseControl这条路,我尝试寻找一种配置简单能快速上手,核心功能也不逊于CruiseControl的工具,Thoughtworks的CruiseControl.rb走入我的视野。

一直关注Thoughtworks,因为以前部门的两位大牛级别的同事都在那任职,另外Thoughtworks推出的产品也的确让我很感兴趣,以前就曾研究过Mingle,只不过Mingle是收费的,而且目前性能还有待提高。CruiseControl.rb(以下称为CC.rb)是Thoughtworks的一款开源作品,其主页上的一小段话很是对我的胃口:"continuous integration isn't rocket science. we keep it simple."。简单,正是我所期望的。

目前CC.rb的最新版本是1.3.0,下载后就是一个压缩包:cruisecontrolrb-1.3.0.tgz。解压后,在你的当前目录下会出现一个cruisecontrolrb-1.3.0的目录。CC.rb的依赖非常之少,你只需要在你的集成服务器上安装上ruby和svn客户端即可,注意:ruby和svn的可执行程序的路径需要添加到你的环境变量的PATH中,否则CC.rb将无法找到ruby和svn。目前CC.rb只是支持Svn,其他版本管理工具尚未得到支持,毕竟CC.rb还年轻,开发时间较少,情有可原。

现在CC.rb已经安装完毕了,没错!CC.rb是用ruby实现的,ruby是脚本类语言,无需编译。你现在就可以进入cruisecontrolrb-1.3.0目录下,执行"cruise –version"(在Unix主机上,你需要先将cruise文件chmod一次,否则cruise将无可执行权限),你会看到:
CruiseControl.rb, version 1.3.0
Copyright (C) 2007 ThoughtWorks

现在你可以添加和配置你的project了。这里要先说一下,CC.rb默认将用户的project数据放在$(HOME)/.cruise/下面,如果你是在Windows平台上,project数据会被默认放在C:\Documents and Settings\USER_NAME\.cruise下面。添加一个proj很简单,你只要提供足够的信息即可:进入到cruisecontrolrb-1.3.0目录下,执行命令:
cruise add PROJ_NAME –url SVN_URL –username USER_NAME –password PASS_WORD,如果你的svn库有权限管理,你需要提供user和passwd。举例: cruise add test_proj –url svn://192.168.0.2:3999/trunk/test_proj –username tony –password tony

这样你就在$(HOME)/.cruise/projects的下面建立了一个test_proj的工程,如果你提供的信息是正确的,CC.rb会在初始建立工程的时候,将你的svn库中的代码checkout出来一份最新的,放在$(HOME)/.cruise/projects/test_proj/work下面。

完成上一步后,$(HOME)/.cruise目录下面有几个配置文件是我们需要重点关注的:
$(HOME)/.cruise
 - config/site_config.rb
 - projects/test_proj/cruise_config.rb

$(HOME)/.cruise/config下的site_config.rb文件是一个全局性的配置文件,无论你在一份CC.rb下建立几个proj,这些proj都会共享该配置,该配置每次修改后都需要重启CC.rb才能生效;该文件中有两个最主要的配置:
1) 邮件服务器配置
如果你想将每次build的结果通过mail的形式发送给相关干系人的话,你就需要配置你的mail server相关信息。
ActionMailer::Base.smtp_settings = {
   :address =>        "xx.xx.xx.xx",
   :port =>           25,   #一般服务器默认smtp端口都是25
   :domain =>         "YOUR_DOMAIN.com",
   :authentication => :plain,
   :user_name =>      "YOUR_USER_NAME",
   :password =>       "YOUR_PASSWD"
 }
2) Dashboard URL
Configuration.dashboard_url = 'http://ip : port'; Dashboard URL将被加入到发给干系人的mail中,这样相关干系人收到mail后可以直接点击该url登录到CC.rb的Dashboard查看相关内容。CC.rb的配置文件也同样适用ruby语言编写的,ruby语言语法较为简单,相信大家都看得懂。

$(HOME)/.cruise/projects/test_proj下的cruise_config.rb文件则是一个Project-specific的local配置文件,它是动态更新的,你的更新在下一次build时是即时生效的。该配置文件中的内容都很重要和实用:
1) project.email_notifier.emails = ['email1@your.site'] 该配置用于添加干系人邮件,这样每次build后,CC.rb都会读取该配置,发送mail给该email list的人;
2) project.email_notifier.from = 'email2@your.site',该配置将指定notify mail中的发件人,你可以因项目的不同而配置不同的mail。
3)  project.rake_task = 'custom',似乎该配置只有当你使用rake这个工具时才需要配置,如果你使用诸如ant,make等工具的话,该配置还需保留原先的被注释状态;
4) project.build_command = 'build_my_app.sh',该配置给予你自定义build脚本的机会,也正是这样,你才可以将CC.rb与Ant, Make等工具集成在一起使用。如:project.build_command = 'make'。注意CC.rb执行build_command的working dir是$(HOME)/.cruise/projects/test_proj/work下面,如果你的build_my_app.sh没有放在work下面,而是在$(HOME)/.cruise/projects/test_proj下面的话,你这块就要配置成:project.build_command = '../build_my_app.sh'; project.build_command与 project.rake_task不能一起使用。
5) project.scheduler.polling_interval = 5.minutes ,CC.rb定期去检测svn是否有new revision,这个配置就是用来指定检测周期的,如果你不配置,那默认是30s。

如果你以上都配置完毕了,你现在就可以启动CC.rb了。启动方法:进入到cruisecontrolrb-1.3.0目录下,执行"cruise start -p port"。CC.rb会启动自带一个轻量级web server – WEBrick ,对于我这个对web server不熟悉的人还是很方便的,我只需要告诉CC.rb端口即可。CC.rb启动后,你可以在浏览器输入http://ip:port,CC.rb的简洁的页面就会显示出来,你会在页面上看到你刚才建立的工程test_proj,点击右上方的"build now"按钮,你就开始了一次build的过程。无论成功还是失败,你都应该收到一封mail,关于此次build的详情可以点击mail中的链接。如果失败了,你可以在页面上的"build log"中看到此次build的详细日志,以帮助你分析失败原因。

CC.rb是如何判断build过程成功还是失败了呢?我们以project.build_command = 'xx'的配置为例,其实CC.rb是通过判断xx这个builder的返回值来决定build过程是否成功的。当xx这个builder返回0,说明build成功;否则build失败;我们不妨测试一下:在$(HOME)/.cruise/projects/test_proj/work下面写一个小程序:
/* test.c */
int main() {
 exit(0);
}
gcc -o test test.c

配置project.build_command = 'test',点击"build now",收到mail,提示build success;如果你将exit(0)改成exit(1),那么一封failed的mail就会发到你的邮箱里。

C/C++没有很好的单元测试工具,一般C/C++单元测试工具都会将单元测试用例编译链接成为一个可执行文件,然后执行,以判断是否通过。到这里你是否想到了如何将C/C++的单元测试与CC.rb集成到一起的方法呢?对,我们通过脚本也好,或者干脆自己编写一个单元测试框架,通过程序返回值告知CC.rb新提交的代码是否通过了单元测试的考验。

以上是第一次使用体验CC.rb时记录下的内容,其实CC.rb就是这么简单!

switch语句性能考量

每年都有应届毕业生来到公司,每年都要对新同事进行代码方面的培训,比如编码规范就是其中之一。编码规范初听起来比较新鲜,但是培训时间长了,显然有些乏味。今年我打算改变策略,让新同事结合已有规范文档和项目代码,自己先挖掘一遍,然后大家通过坐下来讨论的互动方式来加深对规范的理解,每次讨论时间限制在1 hour以内,不给大家打瞌睡的机会^_^。

上周和新同事一起讨论表达式和语句,说到了switch和if,谈到了他们的用途和区别。大家都清楚switch语句被称为多分支语句,当代码中即将出现3个及3个以上分支时,推荐用switch,这样代码可读性好,清晰,格式工整;但是同样switch也是有局限的,就是switch(xx)中的xx必须是整型变量;如果你的条件判断是字符串比较,就无法直接使用switch了。switch的这一局限实际上是有原因的,为什么呢?在于其性能优化。那switch语句在底层到底是如何实现的呢?和if语句相比,switch除了美观之外,优势又在哪里呢?我们唯有到汇编层去看个究竟了。

我们先来看看if多分支的情况://Windows XP + gcc v3.4.2 (mingw-special)
//testif.c
int test_if_performance(int i) {
    int rv = i;

    if (rv == 10) {
        rv += 100;
    } else if (rv == 11) {
        rv += 101;
    } else if (rv == 12) {
        rv += 102;
    } else if (rv == 13||rv == 14 || rv == 15) {
        rv += 105;
    } else {
        rv += 0;
    }
    return rv;
}

我们通过-S选项得到test_if_performance的汇编代码,我们加上了-O2的优化选项:
//gcc -S O2 testif.c
//testif.s
… …
_test_if_performance:
    pushl    %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %edx
    cmpl    $10, %edx
    je    L11
    cmpl    $11, %edx
    je    L12
    cmpl    $12, %edx
    je    L13
    leal    -13(%edx), %eax
    cmpl    $2, %eax
    ja    L3
    addl    $105, %edx
L3:
    popl    %ebp
    movl    %edx, %eax
    ret
    .p2align 4,,7
L11:
    popl    %ebp
    movl    $110, %edx
    movl    %edx, %eax
    ret
    .p2align 4,,7
L12:
    popl    %ebp
    movl    $112, %edx
    movl    %edx, %eax
    ret
    .p2align 4,,7
L13:
    popl    %ebp
    movl    $114, %edx
    movl    %edx, %eax
    ret

从这段汇编码来看,if语句是逐个判断下来的,如果i = 19的话,程序需要从头判断到尾,"一个都不能少"^_^。那么拥有同样语义功能的switch代码又是如何实现的呢?我们继续看下去。
// testswitch.c 这个文件实现的是和上述testif.c同样的功能
int test_switch_performance(int i) {
        int rv = i;

        switch(rv) {
                case 10:
                        rv += 100;
                        break;
                case 11:
                        rv += 101;
                        break;
                case 12:
                        rv += 102;
                        break;
                case 13:
                case 14:
                case 15:
                        rv += 105;
                        break;
                default:
                        rv += 0;
        }
        return rv;
}

我们同样用-O2来得到switch的汇编代码:
//gcc -S O2 testswitch.c
//testswitch.s
… …
_test_switch_performance:
    pushl    %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %ecx
    leal    -10(%ecx), %edx
    movl    %ecx, %eax
    cmpl    $5, %edx
    ja    L2
    jmp    *L10(,%edx,4)
    .section .rdata,"dr"
    .align 4
L10:
    .long    L3
    .long    L4
    .long    L5
    .long    L8
    .long    L8
    .long    L8
    .text
    .p2align 4,,7
L8:
    leal    105(%ecx), %eax
    .p2align 4,,15
L2:
    popl    %ebp
    ret
    .p2align 4,,7
L3:
    popl    %ebp
    leal    100(%ecx), %eax
    ret
    .p2align 4,,7
L4:
    popl    %ebp
    leal    101(%ecx), %eax
    ret
    .p2align 4,,7
L5:
    popl    %ebp
    leal    102(%ecx), %eax
    ret
看完汇编码,第一感觉:cmpl少了许多,一个只读数据段中的L10的标签映入眼帘,以L10标签为起始的内存中依次存储了L3、L4、L5和三个L8的地址,看起来就像是一个地址数组,或者是一个地址表,访问这个数组中的元素实际上就是调用每个元素对应地址中的一段代码。我们继续往前看,来证实一下这个想法。代码不多,比对着汇编指令手册读起来也不甚难。

pushl    %ebp
movl    %esp, %ebp        // 将栈帧地址存在%ebp中
movl    8(%ebp), %ecx        // 将rv值存储到%ecx中
leal    -10(%ecx), %edx        // 将rv值-10之后的值,作为地址偏移量存放到%edx
movl    %ecx, %eax        // 将%ecx中的rv值存储到%eax中
cmpl    $5, %edx            // 比较5 vs. (rv – 10),显然5是编译器经过代码扫描后,算出的一个最大偏移值
ja    L2            // jump if above ,如果5 > %edx中的值,则跳到L2继续执行
jmp    *L10(,%edx,4)        // 如果5 <= %edx中的值,则jmp    *L10(,%edx,4)

解析一下jmp    *L10(,%edx,4),按照书中所说,*L10(,%edx,4)应该对应一个叫indexed memory mode的模式,格式一般是base_address(offset_address, index, size),含义就是base_address + offset_address + index * size;这样似乎就一目了然了。我们拿i = 12为例,经过前面的计算,%edx中存储的是2,L10(,%edx,4)相当于L10 + 0 + 2 * 4,也就是起始地址=L10 + 8的那个内存区域,恰好是L5的起始地址,jmp    *L10(,%edx,4),直接将代码执行routine转到L5了:

L5:
    popl    %ebp
    leal    102(%ecx), %eax
    ret
显然这和前面的猜测是一致的,switch并没有使用性能低下的逐个cmpl的方式,而是形成了一个跳转表(以L10为首地址的地址数组),并将传入switch的那个整型值经过已经的运算后作为offset值,通过一个jmp直接转到目的代码区,这样无论switch有多少个分支,实际上都只是做了一次cmpl,性能照比多if有很大提升。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats