LISA是ARM公司開發的一款開源工具。在內核開發過程中,苦於無法針對修改內容進行一些量化或者可視化結果的測量,而無感。LISA對於模型調優,回歸測試都有較強的支持。
什么是LISA?
LISA是Linux Interactive System Analysis的縮寫,從字面意思可以看出是一個分析工具,具有交互性特點,這有賴於ipython腳本。
LISA是一個Linux環境下用於回歸測試和對於各種workload進行交互測試的工具集。目前LISA主要專注於scheduler、power management和thermal框架的分析。但不僅於此,LISA提供一個框架且可以輕松擴展到其他用途。
LISA提供一些列API用於測試條例編寫和回歸測試條例開發。一系列針對內核核心功能的回歸測試條例已經提供。另外,LISA使用卓越的IPython Notebook框架和一些示例用於進行實驗。
LISA用來干什么?
- 有助於學習已有的功能
- 有助於開發新的代碼
- 有助於發現問題,並且找到原因
- 有助於分享可復制的測試:
- 足夠的彈性保證在不同待測設備上重復同樣的實驗
- 簡化預定義workload的生成和執行
- 定義一系列方法來評估內核行為
- 更簡單的獲取數據文件來生成統計信息和報表
LISA框架結構

待續:devlib/workload/trappy/bart/target什么功能?擴展?
TRAPpy
TRAPpy,即Trace Analysis and Plotting in Python,是一個用於分析數據的可視化工具。它分析類ftrace日志文件,然后基於分析數據創建圖表和數據分析。
TRAPpy需要一些其他工具的支持才能正常工作,比如trace-cmd、kernelshark。trace-cmd用於將trace.dat轉換成trace.txt文件。
TRAPpy安裝
Install additional tools required for some tests and functionalities
sudo apt install trace-cmd kernelshark
安裝pip等工具:
sudo apt install python-pip python-dev
安裝依賴庫文件:
sudo apt install libfreetype6-dev libpng12-dev python-nose
sudo pip install numpy matplotlib pandas ipython[all]
安裝TRAPpy:
sudo pip install --upgrade trappy
TRAPpy使用
啟動一個ipython notebook服務:
ipython notebook
會彈出一個瀏覽器,可以在里面創建,修改,執行腳本。
API文檔:https://pythonhosted.org/TRAPpy/
BART
BART,即Behavioural Analysis and Regression Toolkit,基於TRAPpy,分析kernel輸出的ftrace來診斷當前的行為是否符合預期。
安裝BART
Install additional tools required for some tests and functionalities
$ sudo apt install trace-cmd kernelshark
Install the Python package manager
$ sudo apt install python-pip python-dev
Install required python packages
$ sudo apt install libfreetype6-dev libpng12-dev python-nose $ sudo pip install numpy matplotlib pandas ipython[all] $ sudo pip install --upgrade trappy
ipython[all] will install IPython Notebook, a web based interactive python programming interface. It is required if you plan to use interactive plotting in BART.
Install BART
$ sudo pip install --upgrade bart-py
BART的用途
BART具有廣泛的用途,主要用於幫助開發者進行一些難以測試功能的自動化測試。
內核開發者:確保代碼的正確性
性能優化工程師:圖形化/診斷不同內核版本之間的性能表現。
質量管理、版本工程師:驗證不同模塊/patch集成特性。
API文檔:https://pythonhosted.org/bart-py
devlib
代碼路徑:https://github.com/ARM-software/devlib
devlib提供一個基於Linux操作系統設備,用於交互和獲取測量結果的接口。
wlgen/workload
用於生成各種負荷,目前主要支持rt-app。
LISA使用
准備工作
下載LISA:git clone https://github.com/ARM-software/lisa.git
運行LISA:source init_env,如下:

更新LISA依賴模塊
lisa-update
使用LISA進行測試
lisa-test tests/eas/acceptance.py
使用LISA分析
執行lisa-ipython后,會打開瀏覽器。在瀏覽器中可以通過創建IPython腳本進行LISA相關測試。

LISA代碼分析
下面是LISA根目錄的二級樹,LISA具有明顯的模塊化區分,基於已有的框架可以輕松編寫測試用例tests,編寫自己想要的測試結果ipynb。
可以看出libs/utils提供LISA基礎框架,
libs/devlib支持和待測設備之間交互連接,
測試用例在tests中,
libs/wlgen產生特定workload,
測試結果在results中,
使用ipynb下各種基本進行分析,
ipynb需要的python庫在libs中。
| ├── assets │ └── mp3-short.json ├── init_env source的環境腳本 ├── ipynb 用來分析的ipython腳本 │ ├── chromeos │ ├── energy │ ├── examples │ ├── profiling │ ├── releases │ ├── sched_dvfs │ ├── sched_tune │ ├── scratchpad │ ├── thermal │ └── tutorial ├── libs 各類基礎功能和第三方應用 │ ├── bart │ ├── devlib │ ├── __init__.py │ ├── trappy │ ├── utils │ └── wlgen ├── LICENSE.txt ├── LisaShell.txt ├── logging.conf ├── README.md ├── results 執行結果 │ └── LisaInANutshell_Backup ├── src shell配置文件 │ └── shell ├── target.config 待測設備配置文件 ├── tests 測試用例編寫腳本,host用到的x86和target用到的arm │ ├── eas │ ├── sfreq │ └── stune ├── tools 測試用到的工作 │ ├── arm64 │ ├── armeabi │ ├── LICENSE.perf │ ├── LICENSE.rt-app │ ├── LICENSE.sysbench │ ├── LICENSE.taskset │ ├── LICENSE.trace-cmd │ ├── plots.py │ ├── report.py │ ├── scripts │ └── x86_64 └── Vagrantfile |
下面重點分析libs/utils、libs/wlgen、libs/devlib、libs/trappy、libs/bart,然后如何寫自己的測試用例,並作分析。
libs/utils
class LisaTest是LISA測試用例的基類,調用class TestEnv配置測試環境,調用class Executor生成Executor。
| @classmethod def _runExperiments(cls): """ Default experiments execution engine """ cls.logger.info('Setup tests execution engine...') test_env = TestEnv(test_conf=cls._getTestConf()) 基於test_conf文件生成TestEnv experiments_conf = cls._getExperimentsConf(test_env) cls.executor = Executor(test_env, experiments_conf) 基於experiments_conf生成Executor # Alias executor objects to make less verbose tests code cls.te = cls.executor.te cls.target = cls.executor.target # Execute pre-experiments code defined by the test cls._experimentsInit() 測試前初始化 cls.logger.info('Experiments execution...') cls.executor.run() 測試執行實體 # Execute post-experiments code defined by the test cls._experimentsFinalize() 測試后收尾工作 |
class TestEnv用於配置LISA執行環境,基於target_conf配置待測設備,基於test_conf配置需要針對測試進行設置,還進行工作目錄、測試工具等設置。
| def __init__(self, target_conf=None, test_conf=None, wipe=True, force_new=False): … super(TestEnv, self).__init__() … # Keep track of android support self.LISA_HOME = os.environ.get('LISA_HOME', '/vagrant') self.ANDROID_HOME = os.environ.get('ANDROID_HOME', None) self.CATAPULT_HOME = os.environ.get('CATAPULT_HOME', os.path.join(self.LISA_HOME, 'tools', 'catapult')) # Setup logging self._log = logging.getLogger('TestEnv') # Compute base installation path self._log.info('Using base path: %s', basepath) # Setup target configuration if isinstance(target_conf, dict): self._log.info('Loading custom (inline) target configuration') self.conf = target_conf elif isinstance(target_conf, str): self._log.info('Loading custom (file) target configuration') self.conf = self.loadTargetConfig(target_conf) elif target_conf is None: self._log.info('Loading default (file) target configuration') self.conf = self.loadTargetConfig() self._log.debug('Target configuration %s', self.conf) # Setup test configuration if test_conf: if isinstance(test_conf, dict): self._log.info('Loading custom (inline) test configuration') self.test_conf = test_conf elif isinstance(test_conf, str): self._log.info('Loading custom (file) test configuration') self.test_conf = self.loadTargetConfig(test_conf) else: raise ValueError('test_conf must be either a dictionary or a filepath') self._log.debug('Test configuration %s', self.conf) # Setup target working directory if 'workdir' in self.conf: self.workdir = self.conf['workdir'] # Initialize binary tools to deploy if 'tools' in self.conf: self.__tools = self.conf['tools'] # Merge tests specific tools if self.test_conf and 'tools' in self.test_conf and \ self.test_conf['tools']: if 'tools' not in self.conf: self.conf['tools'] = [] self.__tools = list(set( self.conf['tools'] + self.test_conf['tools'] )) # Initialize ftrace events # test configuration override target one if self.test_conf and 'ftrace' in self.test_conf: self.conf['ftrace'] = self.test_conf['ftrace'] if 'ftrace' in self.conf and self.conf['ftrace']: self.__tools.append('trace-cmd') # Initialize features if '__features__' not in self.conf: self.conf['__features__'] = [] self._init() # Initialize FTrace events collection self._init_ftrace(True) # Initialize RT-App calibration values self.calibration() # Initialize local results folder # test configuration override target one … res_lnk = os.path.join(basepath, LATEST_LINK) if os.path.islink(res_lnk): os.remove(res_lnk) os.symlink(self.res_dir, res_lnk) # Initialize energy probe instrument self._init_energy(True) |
class Executor是實際生成workload的部分,worload的具體情況在experiments_conf中配置。
| def __init__(self, test_env, experiments_conf): """ Tests Executor A tests executor is a module which support the execution of a configured set of experiments. Each experiment is composed by: - a target configuration - a worload to execute The executor module can be configured to run a set of workloads (wloads) in each different target configuration of a specified set (confs). These wloads and confs can be specified by the "experiments_conf" input dictionary. Each (workload, conf, iteration) tuple is called an "experiment". All the results generated by each experiment will be collected a result folder which is named according to this template: results/<test_id>/<wltype>:<conf>:<wload>/<run_id> where: - <test_id> : the "tid" defined by the experiments_conf, or a timestamp based folder in case "tid" is not specified - <wltype> : the class of workload executed, e.g. rtapp or sched_perf - <conf> : the identifier of one of the specified configurations - <wload> : the identified of one of the specified workload - <run_id> : the progressive execution number from 1 up to the specified iterations After the workloads have been run, the Executor object's `experiments` attribute is a list of Experiment objects. The `out_dir` attribute of these objects can be used to find the results of the experiment. """ # Initialize globals self._default_cgroup = None self._cgroup = None # Setup logging self._log = logging.getLogger('Executor') # Setup test configuration 解析experiments_conf,這些參數都會傳遞給wlgen執行。 … self._print_section('Experiments configuration') 打印此實驗配置 … |
Executor.run根據Executor.__init__解析的配置,以workload為單位開始執行。
| def run(self): self._print_section('Experiments execution') self.experiments = [] # Run all the configured experiments exp_idx = 0 for tc in self._experiments_conf['confs']: # TARGET: configuration if not self._target_configure(tc): 配置待測設備 continue for wl_idx in self._experiments_conf['wloads']: # TEST: configuration wload, test_dir = self._wload_init(tc, wl_idx) workload初始化 for itr_idx in range(1, self._iterations + 1): exp = Experiment( wload_name=wl_idx, wload=wload, conf=tc, iteration=itr_idx, out_dir=os.path.join(test_dir, str(itr_idx))) self.experiments.append(exp) # WORKLOAD: execution self._wload_run(exp_idx, exp) worload執行 exp_idx += 1 self._target_cleanup(tc) self._print_section('Experiments execution completed') self._log.info('Results available in:') self._log.info(' %s', self.te.res_dir) |
Executor._wload_run執行單個workload:
| def _wload_run(self, exp_idx, experiment): tc = experiment.conf wload = experiment.wload tc_idx = tc['tag'] self._print_title('Experiment {}/{}, [{}:{}] {}/{}'\ .format(exp_idx, self._exp_count, tc_idx, experiment.wload_name, experiment.iteration, self._iterations)) # Setup local results folder self._log.debug('out_dir set to [%s]', experiment.out_dir) os.system('mkdir -p ' + experiment.out_dir) # Freeze all userspace tasks that we don't need for running tests need_thaw = False if self._target_conf_flag(tc, 'freeze_userspace'): need_thaw = self._freeze_userspace() # FTRACE: start (if a configuration has been provided) if self.te.ftrace and self._target_conf_flag(tc, 'ftrace'): self._log.warning('FTrace events collection enabled') 准備抓取ftrace self.te.ftrace.start() # ENERGY: start sampling 抓取Power Meter數據 if self.te.emeter: self.te.emeter.reset() # WORKLOAD: Run the configured workload wload.run(out_dir=experiment.out_dir, cgroup=self._cgroup) 執行workload 下面是收集Power Meter和ftrace數據。 # ENERGY: collect measurements if self.te.emeter: self.te.emeter.report(experiment.out_dir) # FTRACE: stop and collect measurements if self.te.ftrace and self._target_conf_flag(tc, 'ftrace'): self.te.ftrace.stop() trace_file = experiment.out_dir + '/trace.dat' self.te.ftrace.get_trace(trace_file) self._log.info('Collected FTrace binary trace:') self._log.info(' %s', trace_file.replace(self.te.res_dir, '<res_dir>')) stats_file = experiment.out_dir + '/trace_stat.json' self.te.ftrace.get_stats(stats_file) self._log.info('Collected FTrace function profiling:') self._log.info(' %s', stats_file.replace(self.te.res_dir, '<res_dir>')) # Unfreeze the tasks we froze if need_thaw: self._thaw_userspace() self._print_footer() |
為了盡量降低測試的干擾,引入了freeze_userspace這個flag,這是基於CGroup的freezer子系統實現的。將必須要保留的進程之外的進程,全部凍結。
| critical_tasks = { 'linux': ['init', 'systemd', 'sh', 'ssh'], 'android': [ 'sh', 'adbd', 'init', 'usb', 'transport', # We don't actually need this task but on Google Pixel it apparently # cannot be frozen, so the cgroup state gets stuck in FREEZING if we # try to freeze it. 'thermal-engine' ] } |
在Executor._wload_conf根據wordload配置,調用wlgen生成workload。
| def _wload_conf(self, wl_idx, wlspec): # CPUS: setup execution on CPUs if required by configuration cpus = self._wload_cpus(wl_idx, wlspec) # CGroup: setup CGroups if requried by configuration self._cgroup = self._default_cgroup if 'cgroup' in wlspec: if 'cgroups' not in self.target.modules: raise RuntimeError('Target not supporting CGroups or CGroups ' 'not configured for the current test configuration') self._cgroup = wlspec['cgroup'] if wlspec['type'] == 'rt-app': return self._wload_rtapp(wl_idx, wlspec, cpus) rtapp類型的workload if wlspec['type'] == 'perf_bench': return self._wload_perf_bench(wl_idx, wlspec, cpus) perf_bench類型的workload raise ValueError('unsupported "type" value for [{}] ' 'workload specification' .format(wl_idx)) |
platforms下存放的是不同類型主板的配置文件。
analysis目錄下存放的是針對不同關注點(比如,cpus、eas、frequency、idle)等解析trace.txt的腳本,經過這些腳本處理。ipython Notebook可以生成可視化圖表。
class CpusAnalysis
如何擴展?
需要編寫自己設備的配置文件target.conf:
| { /* Platform */ /* - linux : accessed via SSH connection */ /* - android : accessed via ADB connection */ /* - host : run on the local host */ "platform" : "android", 不同類型的Target,對應不同類型的Connection /* Board */ /* Currently supported boards are: */ /* juno : target is a JUNO board */ /* tc2 : target is a TC2 board */ /* Leave commented if your board is not listed above */ "board" : "hikey", 主板類型對應libs/utils/platforms/hikey.json文件。 /* Target Android device ID */ "device" : "0123456789abcdef", adb設備的ID /* Login username (has to be sudo enabled) */ "username" : "root", … /* Devlib modules to enable/disbale for all the experiments */ "modules" : [], "exclude_modules" : [], … /* List of test environment features to enable */ /* no-kernel : do not deploy kernel/dtb images */ /* no-reboot : do not force reboot the target at each */ /* configuration change */ /* debug : enable debugging messages */ "__features__" : "no-kernel no-reboot" } |
在utils/analysis下,基於class AnalysisModule擴展自己的分析腳本,生成圖表。
libs/wlgen
class Workload作為各種workload的基類,class RTA是class Workload子類。
class Workload的__init__最主要的是進行參數的初始化,使用target.config配置。
run是class Workload的核心,該方法是負荷的執行主體。如果需要抓取ftrace,也會在這里收集。
| def run(self, ftrace=None, cgroup=None, cpus=None, background=False, out_dir='./', as_root=False, start_pause_s=None, end_pause_s=None): self.cgroup = cgroup # Compose the actual execution command starting from the base command # defined by the base class _command = self.command if not _command: self._log.error('Error: empty executor command') # Prepend eventually required taskset command if cpus or self.cpus: 如果需要設置CPU親和性,使用taskset進行設置。 cpus_mask = self.getCpusMask(cpus if cpus else self.cpus) self.taskset_cmd = '{}/taskset 0x{:X}'\ .format(self.target.executables_directory, cpus_mask) _command = '{} {}'\ .format(self.taskset_cmd, _command) if self.cgroup and hasattr(self.target, 'cgroups'): # Get a reference to the CGroup to use _command = self.target.cgroups.run_into_cmd(self.cgroup, _command) # Start FTrace (if required) if ftrace: 設置ftrace相關sysfs節點,啟動ftrace抓取 ftrace.start() # Wait `start_pause` seconds before running the workload if start_pause_s: self._log.info('Waiting %f seconds before starting workload execution', start_pause_s) sleep(start_pause_s) # Start task in background if required if background: 是否作為背景進程運行 self._log.debug('WlGen [background]: %s', _command) self.target.background(_command, as_root=as_root) self.output['executor'] = '' # Start task in foreground else: self._log.info('Workload execution START:') self._log.info(' %s', _command) # Run command and wait for it to complete results = self.target.execute(_command, as_root=as_root) 在class TestEnv的_init_target創建了class Target的子類class AndroidTarget。此處execute都是通過adb shell執行。 self.output['executor'] = results # Wait `end_pause` seconds before stopping ftrace if end_pause_s: self._log.info('Waiting %f seconds before stopping trace collection', end_pause_s) sleep(end_pause_s) # Stop FTrace (if required) ftrace_dat = None if ftrace: 停止ftrace抓取,並導出ftrace內容。 ftrace.stop() ftrace_dat = out_dir + '/' + self.test_label + '.dat' dirname = os.path.dirname(ftrace_dat) if not os.path.exists(dirname): self._log.debug('Create ftrace results folder [%s]', dirname) os.makedirs(dirname) self._log.info('Pulling trace file into [%s]...', ftrace_dat) ftrace.get_trace(ftrace_dat) if not background: self.__callback('postrun', destdir=out_dir) self._log.debug('Workload execution COMPLETED') return ftrace_dat |
在了解了基類Workload之后,稍微了解一下class RTA。
RTA根據需要增加了calibrate,用於在執行rtapp workload之前,校准cpu的性能。
另外擴展了四種任務類型,class Ramp、class Step、class Pulse和class Periodic。
如何擴展?
workload的擴展都是基於class Workload進行。如果需要創建自己的workload,就需要參照rta.py,寫一個自己的子類;class LocalLinuxTarget作為class LinuxTarget的子類,用於測試本地host設備。
libs/devlib
target.py中定義了基類class Target,以及兩種類型的子類class LinuxTarget和class AndroidTarget,針對ssh連接設備和adb連接設備。
class Workload的三個子類,分別對應三種不同類型的連接class AdbConnection、class SshConnection和class LocalConnection。
class FtraceController進行ftrace抓取前buffer大小、filter等的設置,導出ftrace,進行trace.dat到trace.txt的轉變,以及抓取結束后的清理工作。
instrument的__init__.py中定義了基類class Instrument,用於擴展不同類型的測量儀器,一般對應的是物理上存在的設備。
class DaqInstrument、class EnergyProbeInstrument和class HwmonInstrument分別對應DAQ、Energy Probe和hwmon三種設備。
class Module基類用於針對不同模塊進行配置,有的是配置某一模塊的內核sysfs節點,有的是使用命令執行操作。
比如class BigLittleModule,online/offline不同cluster的CPU,或者獲取CPU的各種信息。
class CpufreqModule顯示/設置CPU的governor、最高頻率、當前頻率等等信息。
bin存放devlib用到的可執行文件,比如busybox、trace-cmd等。
如何擴展?
所以綜合下來,在devlib中可能根據class Workload需要擴展不同類型的連接。
如果有新的測試儀器,需要擴展class Instrument。
有時候為了方便對摸快操作,可以基於class Module進行擴展。
libs/trappy
從TRAPpy的縮寫即可知道,一是解析trace,二是對解析結果進行可視化顯示。
在trappy/trappy下有很多python腳本,里面注冊了很多ftrace的解析器,register_ftrace_parser和register_dynamic_ftrace。
libs/bart
在進行了這些分析之后,可以看出test、experiment、workload之間的關系。
一個test可以對應一個或多個experiment;一個experiment可以對應一個或多個workload。
test對應tests目錄中的腳本,experiment對應Executor,workload對應wlgen。
編寫測試用例
編寫分析腳本
參考資料
- LISA Wiki:https://github.com/ARM-software/lisa/wiki
- LISA Git:https://github.com/ARM-software/lisa