从来没有想到过会有一个bug令我愤怒到来这里吐槽。大概算是一种积怨已久吧。
最近在一个玩具前端项目里用了parcel。偶然发现parcel开箱即用地支持CoffeeScript,忽然来了兴致把main.js
重写成了main.coffee
。
不要问我为什么0202年用CoffeeScript,问就是我有病。
parcel工作良好,我开始飘了,又把main.coffee
改成了main.litcoffee
。概括地说这是CoffeeScript内建支持的「文学编程」(Literate programming)格式,类似于Jupyter Notebook但是基于Markdown,编译器会忽略除了缩进代码块以外的所有内容。
这时问题来了:parcel并不支持.litcoffee
拓展名。然而我并没有抱怨反而看到了潜在的PR(,于是对本地的parcel简单地patch了一下,在这里注册了.litcoffee
拓展名并且在这里为具有该拓展名的文件设置好选项literate: true
。紧接着bug就出现了,(剧透:该bug与parcel无关。)main.litcoffee
中的代码被挂在了构建失败的报错信息中。
经过一系列的console.log
,问题不但没有变得明朗,反正扑朔迷离了起来:CoffeeScript的核心编译函数CoffeeScript.compile
被调用了两次,第一次的选项为{ sourceMap: true, literate: true }
,第二次却是{ filename: '<anonymous>', sourceMap: true, literate: false }
,最终由于literate
被错误设置导致编译失败。
为什么一份代码要被编译两次?第二次的选项又为何如此离谱?
实际上,第二次的编译是由babel触发的!惊不惊喜意不意外,babel在把CoffeeScript编译器吐出的代码编译到es5的过程中又(企图)用错误的选项去编译CoffeeScript源文件!它这样做的理由,我相信在座的各位谁都猜不到。
首先被牵扯进来的是工具库@babel/template
。这个库的功能和Python的标准库string.Template
几乎一模一样:将一个含有占位符的字符串作为模板,后续通过使用不同的上下文参数进行替换来生成不同的结果,区别在于@babel/template
接受的上下文参数和生成的结果是语法树节点。出问题的不是这个库的功能,而是这个库的作者认为有人会经常使用模板生成了语法树却不使用它,因此将其实现为惰性生成。惰性本身倒也没什么,问题是好心的作者发现如果生成过程中抛出了异常,惰性生成会导致报错信息的调用栈失去意义,因此在原本应该生成语法树的地方,他捕获了一份当前调用栈信息并保存下来(这真的比生成语法树要快吗……),一旦之后真正生成过程中发生了异常,就把保存下来的调用栈打印出来以供参考。
捕获调用栈的方法采用了喜闻乐见的try throw new Error() catch(err)
惯用法以保证对IE的支持,然而IE<=9即便如此也不行……
回忆一下脚本语言报错时滚滚长江东逝水一样的报错,每层调用栈大致包括:代码路径,行号列号,当前方法名。换言之,对于CoffeeScript,需要sourcemap。这就引出了将bug接力棒交给CoffeeScript的方式:CoffeeScript支持在node环境下「原生」执行(实际上是粗糙的JIT),但是(CoffeeScript的注释说)node并不支持使用sourcemap来转换异常报错信息,所以CoffeeScript只能自己动手。怎么动手呢?直接把Error
原型的方法给劫持了!好家伙,@babel/template
本来只是想捕获一份调用栈,甚至这个调用栈里已经完全没有CoffeeScript的存在了,结果没想到一下子又让CoffeeScript执行了起来。反正当我看到调用栈里CoffeeScript出现在Babel上方的时候我整个人都傻了。
故事到了这里还没有山穷水尽。虽然CoffeeScript错误地假设了整个调用栈都是它的地盘并掌控了报错信息的编写,但是它也不会胡来。交给CoffeeScript的信息包括了每层调用栈原始的路径,这些路径大多是以.js
结尾而被CoffeeScript原样输出。然而,坏就坏在有些在闭包里的层没有源文件路径(为什么?),其文件名被统一制定为<anonymous>
。那么还有谁的文件名叫<anonymous>
呢?当然是我们刚刚编译过的main.litcoffee
!没错,parcel虽然知道它的名字叫main.litcoffee
,但是觉得工具人CoffeeScript并没有必要知道就没告诉它。而CoffeeScript则憨批地用一个全局变量缓存了所有编译过的代码和sourcemap……
终于,最后的槽点要来了:CoffeeScript依次检查它缓存的每一份名为<anonymous>
的代码对应的sourcemap在调用栈行列号对应的位置有没有符号,如果任何一份有符号,就认为这份sourcemap就是我们要的(啊这);如果一份都没匹配上,就将最后一份代码重新编译并生成sourcemap返回……
作为曾经风靡一时的开源项目,CoffeeScript在这里慷慨地附送了大段注释(随便一提,这篇代码就是用文学编程写的),解释道存在着一种情况,最初并没有为某些代码生成sourcemap以提高性能,所以这里为它补上,这样只有非常偶然地抛异常情况下性能会有影响,但是用户体验得到了保障,好耶。
好个屁,只有在最后一份名为<anonymous>
的代码刚好对应着正在输出报错信息的调用栈帧(这意味着所有闭包栈帧最多只有一个能输出准确),并且它之前还恰好没有生成sourcemap的情况下,这个逻辑的结果才是正确的。而且, 你编译代码的时候选项就乱给吗?(万万没想到最后背锅的是CoffeeScript自己)或者退一步讲,只缓存代码有什么用?为什么不缓存编译结果?
总结一下,这个故事讲的是parcel找了两个兄弟来帮用户干活,结果一个把另一个随身携带的雷管()当成了电池然后把用户家给炸了。整个bug构造精巧环环相扣,每一个库都能洗清自己的罪名,最后留下我对着一地鸡毛发出像是见识了社会结构性矛盾一般的叹息。这个故事同时还涉及到(前端)开源项目诸多犯罪行为:
我最想不通的一件事:明明这个bug并不需要这么庞大地多方配合,仅仅依靠一个简单的CoffeeScript片段就能部分复现(调用栈里有两个或以上闭包+一些巧合),为什么这么多年了一个明显错误的报错输出逻辑能一直活下来?
啊,今天也是盼望着Rust早日一统天下的一天呢。
后日谈:搞清楚整个bug机理的我是否准备彻底根除之?不。我只是给parcel调用CoffeeScript的选项里加了一个filename
就准备提PR了。请不要给parcel作者打小报告。(
说一个税务上的难为人的bug
上级税务局:xx纳税人应该享受优惠政策,但是看他报表,他交税了!你们赶紧处理!
县税务局:核实了,纳税人自己填错表了!我们立马退税!
xx纳税人:30多块钱,我不要了,跑来跑去太麻烦!!
上级税务局:你们必须给纳税人退,不然扣你分!
xx纳税人:你们这是为难我们纳税人,不要不要,你要非得让我退!我跑来跑去油费都得200多!!投诉,找借口投诉,扣你分!
县税务局:我去年买了块表!!!
大约2006年的时候,大学寝室一堆人围着安静看电脑(不是那种片,嗯),突然屏幕右下角出现了“有未识别的USB设备”的提示,我们一群人面面相觑:没人动啊。随后不约而同地看向桌子上的一根USB延长线,发现——
一把铁勺子不知为何插在了里面。
——————分割——————
第三篇过千赞回答居然是这个……
居……居然8K了,目瞪口呆.bug
防止宝宝咬手指甲的药水,涂在指甲上,无毒无害无色,但非常非常苦。
我给我2岁半的宝宝涂了之后他手指伸进嘴巴想咬指甲,苦死了,大哭,手指抽出来。
哭几秒还是苦,手指伸回去擦嘴唇,更苦了,大哭,手指抽出来。
哭几秒还是苦,手指伸回去擦嘴唇,更苦了,大哭,手指抽出来。
。。。。。。
不知道大家见过笔记本放在麻将桌上就不能充电的情况没有?
我是给部分客户提供苹果上门服务的。
有一个大哥(各方面都是大哥),在山上住别墅。有一天跟我说他玩扑克牌的MacBook Air不能充电了。我赶紧带上工具上门处理,到了之后一看他在棋牌室的麻将桌上盯着电脑呢,检查发现果然不充电(充电器接口指示灯不亮),赶紧拿到旁边长条桌上逐一检查充电器和笔记本内部是否进水…检查完了发现没问题,充电器是好的,笔记本也是好的,又可以充电开机了。
那就拿了上门费回家吧。
第二天,大哥说又不能充电了,让我买个新的充电器给他。好吧,咱拿了新的45W充电器又上山了,这次是在茶室,又能充电了。我说再查一下原因,大哥说懒得麻烦,换新的就行了,旧的你带回去研究。
第三天没事,第四天也没事,第五天大哥说又不能充电了….
这次去我都想住下来看看到底是怎么回事了!因为我去了后一检查,笔记本是好的,充电器也是好的,也能充电开机。但是大哥反应经常不能充电…
最后还是我让大哥回忆哪些时间哪些地方容易出现不能充电的情况,大哥想了想,指着棋牌室说应该就是那间屋子,他习惯在那里抽烟玩电脑,也就是在那里特别容易出现故障。
然后我就去仔细检查了棋牌室,最后发现问题居然出在这张电动麻将桌上面!
笔记本放在麻将桌上刚好有一半机身压在桌子中间洗牌器上面,这时候就会出现不能充电的情况。如果移动笔记本放在桌子边缘就可以充电。
最后我判断就是电动麻将桌中心的磁铁影响了MacBook Air的电源管理系统或者其他硬件,导致不能充电。
解决方法也简单,加一张小桌子在这个房间,笔记本专门放上面…
最后我只收了一次上门费和几次打车费。
顺便提醒大家注意不要把笔记本放在电动麻将桌上
以前在一个小公司遇到的bug,一个只在阴雨天出现的bug,一个同事至今想起来还激动的拍轮椅
测试环境的服务器是两台实体主机放在我旁边的桌子下面,生产环境用的阿里云服务器。
某天前端开发人员反馈测试环境某些接口调不通,某些接口正常,经过排查,调不通的接口都在同一台测试机器。过去看发现机器居然关机了!
我们很奇怪,周末办公室没有断过电,为什么机器会自动关机?重启大法解决了。
然后这个问题谁都没放在心上,直到过了几天又出现了!
这下开始慌了,是不是谁写的代码里有隐藏的bug导致了机器会关机?
但是代码审查一直没有查到这样的代码,而且几个版本的迭代过程中生产环境从来没有过这样的问题
而这个bug像个梦魇一样,隔那么几天就来一回,而且我们发现每次bug出现的时候都是阴雨天。老板下令一定要找出原因,不然每次新版本上线都胆战心惊,怕问题蔓延到生产环境
于是我把两台服务器从桌子下拉出来摆在我脚边,就是要看看到底什么原因
后来...
.
.
.
.
.
.
.
.
.
.
后来问题找到了,天气一冷坐我对面的家伙就把腿抬起来放到测试服务器主机上,那里出风口暖和
不小心就把插头搞接触不良断电了
我们把他腿打断之后这个问题再也没出现过
猜测,可能对象没初始化。
2019.1.26
·按照 @因为你会发光啊 的建议给图片做了标注
·从评论区来看这居然是个很普遍的坑?
—————————原回答—————————
不是自己的电脑。
昨天系统爆炸了,重装系统后发现只要调低屏幕亮度,电脑就会休眠,以为键盘映射也会出问题,不断重装无果。
心好累,就擦了下电脑。然后发现,
什么破键盘膜。
某天我在玩wii的时候过热自动关机了,看了下发现主机的风扇不转了,百度之发现很多人都说不转,什么温度到多少多少度才转还说某些主机有bug温度检测不行什么的。
然后准备动手拆机,期间我还在想先看看风扇是不是好的,要是主板不给风扇供电就从usb口取电什么的。结果拆开一开
真的有BUG!
策略类游戏的玩家,尤其是文明系列和p社系列的玩家都知道,这类游戏大量的数值计算会导致游戏越到后期运行越慢,特别是p社祖传的单核引擎,一核有难7核点赞,《群星》1000星地图的一局游戏往往到游戏内100-150年左右就开始减速运行,一般的电脑基本在游戏内1天需要1s的程度,更别说200年以后突然出现的规模庞大的天灾了。
一般来说这种减速就是大量的运算必然带来的,基本没什么可能去(很容易地)优化,不过有玩家发现,当群星的后期游戏中,你选中一支规模很大(200艘船以上)并且“已经完全升级完毕”的舰队的时候,游戏会突然出现极大的卡顿,原来40帧变20帧,原来20帧左右的游戏一下子就变成只有5-6帧,而当你不再选中这支舰队,或者选中的是“还可以进行升级”的舰队的时候,游戏的运行就很正常(尽管也不快),显然这种卡顿应该是一个程序设计上的问题,而不是游戏本身积累的运算量带来的。
最后,这个问题由贴吧大佬成功破案,不过在破案之前,我们先想想正常人要让玩家知道舰队还可不可以进行升级会怎么写这个程序。
首先游戏的逻辑是这样的:一个舰队里有很多的船,分成护卫驱逐巡洋战列等等不同的种类,配船器在每个种类下都保存着一个或多个模板,然后太空港根据模板来建造舰船,当获得了新科技想用在船上的时候,要先在配船器里修改模板,然后再把舰队拉到太空港点击上面图里的升级按钮——显然,升级按钮只有在舰船可以升级的时候才能点亮,而程序又怎么知道舰队里有船能升级呢?
每次选到舰队,要弹出界面告诉玩家升级情况了,就一艘艘船挨个查过去跟原来的模板对应啊!如果全对上了就告诉没升级,如果有对不上的就告诉有升级,完了!
if 所有舰船的配置都与模板相同 return 没有可用升级 else return 有可用升级 endif
——就算没学过编程,一般人稍微想一下也都知道是吧。接触过编程的可能说会不会效率太低,不过反正更底层的代码说舰队里最多塞500条船,查一遍也不会太慢是吧?
paradox蠢驴的脑回路和一般人不一样。
虽然他们也是一艘艘挨个查的,但是一想到查完了怎么办他们就思维短路了,他们让这个循环在查到了有升级之后,return有升级、结束,没问题;但是查到没升级呢?不知道是害怕查错了还是怎么的,蠢驴程序猿们让这个循环返回去再查一遍……
也就是说,如果你选中了一个已经完全升级的舰队,游戏后台就会一直跑着这个查模板的循环……
你说看上去不怎么样?确实,在游戏的前期和中期,计算量还不是特别大的时候,CPU0随便拿点运行时出来跑这个循环,基本看不出对性能的影响,而且游戏的前期大家都是屌丝,舰队规模小,跑这个循环更没有影响了。
但是到了后期,大家地盘也大了、舰队也多了,本来CPU0就不堪重负,玩家一选中舰队,查模板的循环一跑就是几百条船,还翻来覆去的跑,快压死的骆驼身上放上了一大捆稻草,剩下的七个骆驼看了摊摊手(尥尥蹶子?)跑了。
你说能不卡吗?