code?is?dumb,?codemap?is?clever
前言
閱讀源代碼是軟件工程師進階的必備技能,但是現目前通過IDE閱讀源代碼的方式卻令人痛苦不已。單行的代碼大家都認識,初級的語法大多都不構成真正的障礙,然而閱讀一個成熟或者開源項目,卻往往令人倍感頭疼。
復雜的嵌套關系,冗余的依賴結構,往往使只希望研究項目核心機制的我們,迷失在代碼的汪洋之中。
?codemap是一款支持自動跳轉,通過連線、高亮、標注等方式輔助閱讀源代碼的工具,通過?codemap,閱讀源代碼不再困難。
AnimatedDrawings 是facebook實驗室發表的論文《A Method for Animating Children's Drawings of the Human Figure》的附屬代碼倉庫,如果通過IDE來分析源代碼,會讓人倍感痛苦,但通過?codemap你會發現閱讀源代碼原來so easy!
一、安裝
官網的readme文檔中有詳實的安裝教程,按照指引能在本地快速搭建起項目的運行環境。但是,在2023.9.1,筆者在mac m2環境按照安裝教程執行
pip install -e .
命令時,會報如下錯誤
Collecting PyYAML==6.0 (from animated-drawings==0.0.0)
? Using cached https://pypi.tuna.tsinghua.edu.cn/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz (124 kB)
? Installing build dependencies ... done
? Getting requirements to build wheel ... error
? error: subprocess-exited-with-error
? × Getting requirements to build wheel did not run successfully.
? │ exit code: 1
? ╰─> [48 lines of output]
? ? ? running egg_info
? ? ? writing lib/PyYAML.egg-info/PKG-INFO
? ? ? writing dependency_links to lib/PyYAML.egg-info/dependency_links.txt
? ? ? writing top-level names to lib/PyYAML.egg-info/top_level.txt
? ? ? Traceback (most recent call last):
? ? ? ? File "/Users/guopengshan/anaconda3/envs/animated_drawiings/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
? ? ? ? ? main()
? ? ? ? File "/Users/guopengshan/anaconda3/envs/animated_drawiings/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
? ? ? ? ? json_out['return_val'] = hook(**hook_input['kwargs'])
? ? ? ? File "/Users/guopengshan/anaconda3/envs/animated_drawiings/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 118, in get_requires_for_build_wheel
? ? ? ? ? return hook(config_settings)
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 355, in get_requires_for_build_wheel
? ? ? ? ? return self._get_build_requires(config_settings, requirements=['wheel'])
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 325, in _get_build_requires
? ? ? ? ? self.run_setup()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 341, in run_setup
? ? ? ? ? exec(code, locals())
? ? ? ? File "<string>", line 288, in <module>
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/__init__.py", line 107, in setup
? ? ? ? ? return distutils.core.setup(**attrs)
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 185, in setup
? ? ? ? ? return run_commands(dist)
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 201, in run_commands
? ? ? ? ? dist.run_commands()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 969, in run_commands
? ? ? ? ? self.run_command(cmd)
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 1233, in run_command
? ? ? ? ? super().run_command(command)
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 988, in run_command
? ? ? ? ? cmd_obj.run()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 319, in run
? ? ? ? ? self.find_sources()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 327, in find_sources
? ? ? ? ? mm.run()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 549, in run
? ? ? ? ? self.add_defaults()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 587, in add_defaults
? ? ? ? ? sdist.add_defaults(self)
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/sdist.py", line 113, in add_defaults
? ? ? ? ? super().add_defaults()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/command/sdist.py", line 251, in add_defaults
? ? ? ? ? self._add_defaults_ext()
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/command/sdist.py", line 336, in _add_defaults_ext
? ? ? ? ? self.filelist.extend(build_ext.get_source_files())
? ? ? ? File "<string>", line 204, in get_source_files
? ? ? ? File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/cmd.py", line 107, in __getattr__
? ? ? ? ? raise AttributeError(attr)
? ? ? AttributeError: cython_sources
? ? ? [end of output]
? note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error
× Getting requirements to build wheel did not run successfully.
│ exit code: 1
╰─> See above for output.
通過debug,發現是PyYAML依賴版本異常導致,只需要將PyYAML升級為6.0.1即能恢復正常,如下圖修改setup.py文件即可
二、IDE 實戰分析
安裝正常后,按照文檔說明,在本地運行
from animated_drawings import render
render.start('./examples/config/mvc/interactive_window_example.yaml')
便能啟動基礎范例。
通常,我們會通過入口文件,在IDE中逐個文件或函數查看代碼的具體實現機制。
比如 render.start('./examples/config/mvc/interactive_window_example.yaml') 這段代碼,首先調用了配置文件interactive_window_example.yaml,然后通過 render 的 start 方法啟動。
接著,我們會發現 interactive_window_example.yaml 又調用了 char_cfg.yaml、dab.yaml、fair1_ppf.yaml 文件,但各個文件的具體功能對于初學者的我們卻無從得知。render.start 方法首先初始化了 cfg 實例,然后通過 cfg 中的具體配置,分別創建了 view、scene、controller 等實例,最后通過 controller.run() 啟動。
在 controller.run 中首先執行了 self._prep_for_run_loop(),然后通過檢測是否 self._is_run_over() 執行死循環,包括 self._start_run_loop_iteration()、self._update()、self._render()、self._tick()、self._handle_user_input()、self._finish_run_loop_iteration() 等更新算法,最后執行 self._cleanup_after_run_loop()。
而針對更具體的 self._prep_for_run_loop()、self._prep_for_run_loop() 等更新算法的實現機制,我們發現他們在 Controller 類中都是通過抽象方法 @abstractmethod 定義的,也就是說對于具體的執行流程,要查看具體執行的函數需要看在函數的運行流程中,具體是哪個 Controller 的子類被實例化了,那個子類實現的 self._prep_for_run_loop()、self._prep_for_run_loop() 等方法才是真實被執行的代碼,因此,我們需要跳轉到 ?render.py 文件中,查看 controller 的實例過程。
通過代碼,我們發現controller的具體實現類是由 cfg.mode 決定,即當 config 中 mode 為 video_render 時,controller 為 VideoRenderController 的實例,當 mode 為 interactive 時,controller 為 InteractiveController 的實例。也就是說,具體執行的 self._prep_for_run_loop()、self._prep_for_run_loop() 等更新算法由 config 中的 mode 決定,當 mode 為 video_render 時,程序流中執行的就是 VideoRenderController 中的 self._prep_for_run_loop()、self._prep_for_run_loop(),否則就是 InteractiveController 中的 self._prep_for_run_loop()、self._prep_for_run_loop()。而具體的 mode 值,在 interactive_window_example.yaml 中,interactive_window_example.yaml 又依賴 char_cfg.yaml、dab.yaml、fair1_ppf.yaml,我們需要不停地在不同的文件夾中打開不同的文件,不停地查找、切換,才能驗證這個最簡單的范例究竟是怎么執行的。
為了這么簡單的需求,我們不停地打開文件,不停地切換,對于初學者,大腦早就一團亂麻,不必要的思維負擔已經消耗了絕大多數的腦力,而對于更重要的具體更新機制,卻還一籌莫展。
code is dumb,這就是?codemap需要解決的問題!
三、?codemap 實戰分析
?codemap摒棄了tab頁的打開方式,通過平鋪布局,以及連線、高亮、標注等一系列手段,輔助用戶閱讀源代碼,告別了為了研究代碼的執行機制,而不停切換文件,不斷展開、折疊源代碼的歷史,為用戶提供了一種可以清晰展示代碼邏輯結構、添加高亮備注的方式。
?codemap目前已經支持js、ts、c、c++、java、golang、python等多種主流編程語言,未來還將支持更多。
單行的代碼大家都認識,但是復雜的項目為什么就看不懂了呢?其中的關鍵在于閱讀源代碼的過程中,我們的大腦做了太多的無用功,在現有的ide中需要記憶大量無關緊要的中間函數、路徑,甚至所在代碼的行數,而通過codemap,我們能很好地解決這個問題。
1、回到上述問題,cfg.mode的具體取值影響了程序流,但在用戶顯式加載的配置文件interactive_window_example.yaml等中并沒有mode配置項,著眼于程序示例1(藍色標記),發現Config類會加載默認配置 mvc_base_cfg.yaml,在該文件中有配置項MODE: 'interactive',因此可以證實controller是InteractiveController的實例。
2、在程序示例2(藍色標記)處,展示了controller.run()的執行步驟,但是具體的執行函數在Controller父類中都是抽象方法,具體的實現在子類InteractiveController中。
3、如圖示例3(藍色標記),InteractiveController會依次執行:
_prep_for_run_loop:更新self.prev_time為當前時間
_is_run_over:通過glfw.window_should_close(self.view.win)來判斷是否需要執行以下循環
_start_run_loop_iteration:調用self.view.clear_window(),實際是通過OpenGL清理窗口
_update:通過平移、旋轉、縮放矩陣相乘,求最新的矩陣
_render:首先通過view._update_shaders_view_transform()等更新camera信息,然后調用scene.draw()渲染
_tick:更新時間
_handle_user_input:處理用戶輸入
_finish_run_loop_iteration:交換緩存
_cleanup_after_run_loop:執行清理程序
四、code is dumb, ?codemap is clever
通過?codemap ?https://codemap.info 進行分析,我們已經大致地了解了AnimatedDrawings的啟動流程,而且期間不會再因為不停地打開、切換文件,不停折疊代碼、不停跳轉而煩惱了,這次進行了分析,下次還能在今天的基礎上繼續研究。
通常,對于越復雜的項目,?codemap的效果越好,想象一下,你要打開幾十個文件,每個文件里面都是你不熟悉的類和函數,對于他們之間的調用、依賴關系,如果純用腦力,我相信你會爆炸的。