Chronaxis用户手册English
本页目录

chronaxis.fast.session

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""一个fast会话的多文件调度和最终累计。23worker只求当前帧贡献;主进程控制统一预算、交付顺序及成功发布前的完整性。4concatenate可暂停可选预读;time_order必要暂存完成后稳定归并。临时目录和5进程均属于本次调用,异常不返回部分结果。内联与batch不经传输协议。6"""7from hashlib import sha2568import heapq9import json10import multiprocessing11from multiprocessing.connection import wait12import os13from pathlib import Path14from tempfile import mkdtemp15from time import perf_counter1617from ..results.budget import ResourceBudget, _reserve_owned_ram, _release_owned_ram, _cleanup_owner18from ..results.errors import BudgetExceededError19from ..dbc_loading import load_dbc_file20from .._control_wire import ControlChannel21from .blocks import RAM, LIMIT, encode, decode, batches22from .compute import _Accumulator, _check_program_spec23from .stream import FileContributions242526def effective_spec(spec, process_count=None):27    """取得不修改原声明的执行视图。参数: spec字典,可选正整数覆盖。返回: 新dict。28    异常: 非正原生整数;不接受bool、字符串或浮点数。29    """30    count = spec.get('process_count', 1) if process_count is None else process_count31    if type(count) is not int or count < 1:32        raise ValueError('process_count需要大于等于1的整数,不接受bool')33    return dict(spec, process_count=count)343536def _worker_process_limit(process_count: int, file_count: int) -> int:37    """从已验证的进程上限和文件数取得文件工作进程数,不包含主进程。3839    参数为正整数。返回至多文件数的工作进程上限;只能并发处理一份时返回0,40    表示在当前进程执行。时间归并的内联暂存不改变这个判定;无输入或输出操作。41    """42    count = min(process_count, file_count)43    return count if count > 1 else 0444546def file_specs(spec):47    """展开有限文件声明。参数: 会话spec。返回: 每文件字典列表。异常: 无输入IO。"""48    inputs = spec.get('inputs', [dict(path=spec['path'], offset_ns=spec.get('offset_ns', 0),49                                   asc_options=spec.get('asc_options', {}))])50    common = {key: value for key, value in spec.items() if key not in ('inputs', '_database')}51    return [dict(common, **item) for item in inputs]525354def _checked_file_specs(spec, programs_checked):55    """取得实际文件声明,并复用本会话已经检查过的同一组公式。5657    参数: 本次独立的spec字典、公共编译器是否已检查顶层公式。58    返回: 文件声明列表;低层inputs覆盖的公式仍检查原文本和指令。59    异常: 非法公式或声明错误;不因顶层已编译就放行另一组覆盖值。60    """61    original = spec['metrics']62    prepared = original if programs_checked else [_check_program_spec(item) for item in original]63    # 缓存只活到准备结束,保留源对象以避免身份复用,不跨会话或按文本猜等价。64    checked = {id(original): (original, prepared), id(prepared): (prepared, prepared)}65    spec['metrics'] = prepared66    specs = file_specs(spec)67    for item in specs:68        actual = item['metrics']69        cached = checked.get(id(actual))70        if cached is None or cached[0] is not actual:71            cached = (actual, [_check_program_spec(metric) for metric in actual])72            checked[id(actual)] = cached73        item['metrics'] = cached[1]74    return specs757677class SessionAccumulator:78    """只有主会话持有的标量累计器。计数、目标缺失和帧身份按计算项分别核验。"""7980    def __init__(self, spec):81        """准备纯累计状态。参数: 已核验声明。返回: 新对象。异常: 名称或单位字段缺失。"""82        self.metrics = [_Accumulator(item) for item in spec['metrics']]83        self.raw = 084        self.targets = [0] * len(self.metrics)85        self.valid = [0] * len(self.metrics)86        self.selected = [0] * len(self.metrics)87        self.kinds = [0] * len(self.metrics)88        self.compatible = set()89        self.profile = spec.get('_profile', False)90        self.accumulate_seconds = 0.09192    def consume(self, record):93        """按输入顺序逐项相加。参数: 一帧贡献。返回: None。异常: 累计溢出。"""94        started = perf_counter() if self.profile else 095        for index, value, keep in record[2]:96            self.metrics[index].add(value, keep)97        if self.profile:98            self.accumulate_seconds += perf_counter() - started99100    def finish_file(self, facts):101        """合并已关闭文件的事实。参数: 文件summary。返回: None。异常: 计数或身份非法。"""102        if type(facts['raw']) is not int or facts['raw'] < 0:103            raise ValueError('文件原始记录计数非法')104        self.raw += facts['raw']105        for field in ('targets', 'valid', 'selected', 'kinds'):106            values = facts[field]107            if len(values) != len(self.metrics) or any(type(v) is not int or v < 0 for v in values):108                raise ValueError('文件计算项计数非法')109        for i in range(len(self.metrics)):110            if not facts['selected'][i] <= facts['valid'][i] <= facts['targets'][i] <= facts['raw']:111                raise ValueError('文件目标/有效/入选计数矛盾')112            self.targets[i] += facts['targets'][i]113            self.valid[i] += facts['valid'][i]114            self.selected[i] += facts['selected'][i]115            self.kinds[i] |= facts['kinds'][i]116            if self.kinds[i].bit_count() > 1:117                raise ValueError(self.metrics[i].name + '跨文件匹配多种实际帧型,请明确选择')118        self.compatible.update(facts['compatible'])119120    def result(self):121        """核完整消费后生成业务行。参数: 无。返回: (行list, 提示list)。异常: 贡献计数不符。"""122        rows = []123        for i, metric in enumerate(self.metrics):124            if (metric.matched, metric.selected) != (self.valid[i], self.selected[i]):125                raise ValueError(metric.name + '贡献未完整且唯一消费')126            row = metric.result()127            if row['value'] is None:128                row['reason'] = ('empty_file' if not self.raw else129                                 'no_target_in_domain' if self.targets[i] else 'no_target_frames')130            rows.append(row)131        warnings = [f'{identifier:#x} 按经典 CAN 实录解码 DBC 的 FD 声明' for identifier in sorted(self.compatible)]132        return rows, warnings133134135class FilePool:136    """管理有限工作槽及每文件连续块前缀;不在内存保存所有暂存块描述。"""137138    def __init__(self, specs, budget, directory, cancel, statistics, *, _programs_checked=False):139        """初始化但不启动。参数: 文件spec列表、共享账本、暂存目录、外部取消、统计dict。140        返回: 新调度器。异常: 无;start安装进程资源的关闭责任。141        _programs_checked只来自父会话的实际准备结果,不读取配置中的标志。142        """143        self.specs, self.budget, self.directory = specs, budget, Path(directory)144        self._budget, self._held_cost = budget, 0145        self._started = self._closing = self._closed = False146        self._programs_checked = _programs_checked147        self.cancel, self.stats = cancel, statistics148        self.context = multiprocessing.get_context('spawn')149        self.stop = self.context.Event()150        self.slots = []151        self.files = [dict(produced=0, consumed=0, spool=0, eof=None, packet=None,152                           last_sequence=-1, last_time=None) for _ in specs]153        self.current = 0154        self.next_file = 0155        self.ordered = specs[0].get('file_merge', 'concatenate') == 'time_order'156        self.worker_cost = 0157        self.digest = sha256(Path(specs[0]['dbc']).read_bytes()).hexdigest()158        if self.digest != specs[0].get('_dbc_digest', self.digest):159            raise ValueError('编译后DBC内容变化')160161    def check_cancel(self):162        """传递外部取消。参数: 无。返回: None。异常: InterruptedError。"""163        if self.cancel is not None and self.cancel.is_set():164            if self.stop is not None:165                self.stop.set()166            raise InterruptedError('fast会话取消')167168169    def start(self, count):170        """预付真实工作槽后启动;任何部分构造失败仍由本池关闭已登记资源。171172        参数: count实际工作数。返回: None。异常: 原预算/启动/归属错误;173        持续清理失败在原异常的fast_pool_cleanup_owner保留内部恢复入口。174        """175        from .session_worker import worker_main176        if self._started or self._closing or self._closed:177            raise RuntimeError('fast工作池已经启动或关闭,不能重复启动')178        self._started = True179        try:180            per_worker = 3 * RAM + Path(self.specs[0]['dbc']).stat().st_size * 64181            _reserve_owned_ram(self, per_worker * count, 'worker_cost')182            for _ in range(count):183                self.check_cancel()184                # 先安装槽,再创建管道;第二根管道或启动失败时,第一根仍有关闭责任。185                slot = dict(control=None, data=None, child_control=None, child_data=None,186                            process=None, tree=None, startup=None, phase='idle', file=None, stop_requested=False)187                self.slots.append(slot)188                slot['control'], slot['child_control'] = self.context.Pipe()189                slot['control'] = ControlChannel(slot['control'],'fast',parent=True,max_bytes=self.budget.memory_budget_bytes)190                slot['child_control'] = ControlChannel(slot['child_control'],'fast',parent=False,max_bytes=self.budget.memory_budget_bytes)191                slot['data'], slot['child_data'] = self.context.Pipe(duplex=False)192                from .._process_start import spawn_entry193                try:194                    target,args,slot['startup']=spawn_entry(self.context,worker_main,195                        (slot['child_control'],slot['child_data'],self.stop))196                except BaseException as error:197                    slot['startup']=getattr(error,'process_start_cleanup_owner',None)198                    raise199                process = self.context.Process(target=target,args=args)200                slot['process'] = process201                process.start()202                from ..batch.process_tree import ProcessTree203                try:204                    slot['tree'] = (ProcessTree(process.pid) if slot['startup'] is None else205                        ProcessTree(process.pid,registration=slot['startup'],process=process))206                except BaseException as error:207                    slot['tree'] = getattr(error,'process_tree_cleanup_owner',None)208                    raise209                if slot['startup'] is not None:210                    slot['startup'].close();slot['startup']=None211                self.stats['worker_pids'].append(process.pid)212                # 子进程已取得自己的端点,父进程关闭副本;失败端点留在同一槽供重试。213                for key in ('child_control', 'child_data'):214                    connection = slot[key]215                    try:216                        connection.close()217                    finally:218                        if connection.closed:219                            slot[key] = None220                self.assign(slot)221        except BaseException as error:222            _cleanup_owner(self, error, 'fast_pool_cleanup_owner', 'fast工作池启动')223            raise224225    def assign(self, slot):226        """按文件声明顺序派发下一文件。参数: 空闲槽。返回: None。异常: 断管。"""227        slot['phase'] = 'idle'228        if self.next_file < len(self.specs):229            index = self.next_file230            self.next_file += 1231            slot.update(file=index, phase='computing')232            operation = 'program_file' if self._programs_checked else 'file'233            slot['control'].send((operation, index, self.specs[index], str(self.directory), self.digest))234235    def grant(self, slot):236        """授权一个完整块的唯一交付路径。参数: 已报价工作槽。返回: None。异常: 必要暂存超额。"""237        if slot['phase'] != 'offered':238            return239        if not self.ordered and slot['file'] == self.current:240            slot.update(phase='direct', delivered=False, received=None)241            slot['control'].send(('direct',))242            return243        size = slot['size']244        quota = self.budget.spool_quota_bytes245        if quota is not None and self.budget.spool_bytes + size > quota:246            if self.ordered:247                raise BudgetExceededError('time_order必要暂存超过spool_quota_bytes')248            if not slot.get('paused', False):249                self.stats['prefetch_pauses'] += 1250                slot['paused'] = True251            return252        self.budget.reserve_spool(size)253        slot['phase'] = 'spooling'254        slot['control'].send(('spool',))255256    def pump(self, blocking=True):257        """推进有限控制和数据消息,优先响应所有worker错误。参数: 是否最多等50ms。258        返回: None。异常: 协议、worker退出、取消或IO错误。259        """260        self.check_cancel()261        for slot in self.slots:262            self.grant(slot)263        connections = [s['control'] for s in self.slots]264        connections += [s['data'] for s in self.slots if s['phase'] == 'direct' and s['received'] is None]265        started = perf_counter()266        ready = wait(connections, timeout=0.05 if blocking else 0)267        self.stats['main_wait_seconds'] += perf_counter() - started268        for slot in self.slots:269            process = slot['process']270            message = None271            pipe_error = None272            if slot['control'] in ready:273                try:274                    message = slot['control'].recv()275                except (EOFError, BrokenPipeError) as error:276                    pipe_error = error277                # 子进程可在发送error后正常退出,完整业务错误必须先于退出状态报告。278                if message is not None and message[0] == 'error':279                    self.message(slot, message)280            if process.exitcode is not None:281                raise RuntimeError(f'fast工作进程意外退出:{process.pid},{process.exitcode}') from pipe_error282            if pipe_error is not None:283                raise pipe_error284            if message is not None:285                self.message(slot, message)286            if slot['phase'] == 'direct' and slot['received'] is None and slot['data'] in ready:287                packet = slot['data'].recv_bytes(LIMIT)288                if len(packet) != slot['size']:289                    raise ValueError('直传贡献长度不符')290                slot['received'] = packet291                self.stats['received_bytes'] += len(packet)292            if slot['phase'] == 'direct' and slot['received'] is not None and slot['delivered']:293                state = self.files[slot['file']]294                state['packet'] = slot['received']295                state['produced'] += 1296                slot.update(phase='consuming', received=None)297            self.grant(slot)298299    def message(self, slot, message):300        """核验工作槽状态及块序号。参数: 槽/控制tuple。返回: None。异常: 错误或协议不符。"""301        tag = message[0]302        if tag == 'error':303            raise RuntimeError(f"{self.specs[slot['file']]['path']}:{message[1]}:{message[2]}")304        if len(message) < 3 or message[1] != slot['file']:305            raise ValueError('工作进程文件身份不符')306        state = self.files[slot['file']]307        number = message[2]308        if type(number) is not int or number != state['produced']:309            raise ValueError('贡献块缺失、重复或顺序错误')310        if tag == 'offer' and slot['phase'] == 'computing':311            size = message[3]312            if type(size) is not int or not 0 < size <= LIMIT:313                raise ValueError('贡献块报价长度非法')314            slot.update(phase='offered', size=size, paused=False)315        elif tag == 'stored' and slot['phase'] == 'spooling':316            if message[3] != slot['size'] or state['spool'] != number:317                raise ValueError('暂存提交长度或前缀不符')318            state['spool'] += 1319            state['produced'] += 1320            self.stats['spool_written_bytes'] += message[3]321            self.stats['spool_live_bytes'] += message[3]322            self.stats['spool_peak_bytes'] = max(self.stats['spool_peak_bytes'], self.stats['spool_live_bytes'])323            slot['phase'] = 'computing'324            slot['control'].send(('next',))325        elif tag == 'delivered' and slot['phase'] == 'direct' and not slot['delivered']:326            if message[3] != slot['size']:327                raise ValueError('直传提交长度不符')328            slot['delivered'] = True329        elif tag == 'eof' and slot['phase'] == 'computing':330            if state['eof'] is not None:331                raise ValueError('重复文件EOF')332            state['eof'] = message[3]333            self.stats['files'].append(dict(file_index=slot['file'], pid=message[5], **message[4]))334            self.assign(slot)335        else:336            raise ValueError('贡献交付状态与消息不符')337338    def packet_records(self, index, number, packet):339        """验证块间顺序并展开一块。参数: 文件/块号/bytes。返回: 贡献生成器。异常: 顺序或协议错误。"""340        state = self.files[index]341        records = decode(packet, index, number, len(self.specs[index]['metrics']))342        if self.specs[index].get('_profile', False):343            started = perf_counter()344            records = list(records)345            self.stats['main_decode_seconds'] += perf_counter() - started346        for record in records:347            timestamp, sequence, _ = record348            if sequence <= state['last_sequence']:349                raise ValueError('贡献记录跨块重复或回退')350            if self.ordered and state['last_time'] is not None and timestamp < state['last_time']:351                raise ValueError('贡献时间跨块回退')352            state['last_sequence'], state['last_time'] = sequence, timestamp353            yield record354355    def read_spool(self, index, number):356        """读取并删除一个已提交块。参数: 文件/块号。返回: bytes。异常: 缺失、超长、IO。357        删除仅减少当前占用,不返还累计预约额度。358        """359        path = self.directory / f'{index}-{number}.block'360        started = perf_counter()361        with path.open('rb') as stream:362            packet = stream.read(LIMIT + 1)363        if len(packet) > LIMIT:364            raise ValueError('暂存块过大')365        path.unlink()366        self.stats['spool_live_bytes'] -= len(packet)367        self.stats['main_spool_read_seconds'] += perf_counter() - started368        return packet369370    def concatenate(self, accumulator):371        """消费当前文件,后续文件同时预读。参数: 会话累计器。返回: None。异常: 运行或累计错误。"""372        for index, state in enumerate(self.files):373            self.current = index374            while True:375                self.pump(False)376                number = state['consumed']377                direct = False378                if number < state['spool']:379                    packet = self.read_spool(index, number)380                elif state['packet'] is not None:381                    packet, state['packet'] = state['packet'], None382                    direct = True383                elif state['eof'] is not None:384                    if number != state['produced']:385                        raise ValueError('文件EOF前贡献未完整消费')386                    accumulator.finish_file(state['eof'])387                    break388                else:389                    self.pump()390                    continue391                for record in self.packet_records(index, number, packet):392                    accumulator.consume(record)393                packet = None394                state['consumed'] += 1395                if direct:396                    slot = next(s for s in self.slots if s['file'] == index)397                    slot['phase'] = 'computing'398                    slot['control'].send(('next',))399400    def file_records(self, index):401        """逐块读取一个已完整暂存文件。参数: 文件号。返回: 贡献生成器。异常: 缺块/完整性错。"""402        state = self.files[index]403        while state['consumed'] < state['produced']:404            self.check_cancel()405            number = state['consumed']406            packet = self.read_spool(index, number)407            yield from self.packet_records(index, number, packet)408            state['consumed'] += 1409410    def time_order(self, accumulator):411        """完成必要暂存后k路稳定归并。参数: 累计器。返回: None。异常: 输入/资源/计算错误。"""412        while any(state['eof'] is None for state in self.files):413            self.pump()414        self.close(True)415        started = perf_counter()416        streams = [iter(self.file_records(i)) for i in range(len(self.files))]417        heap = []418        for index, stream in enumerate(streams):419            record = next(stream, None)420            if record is not None:421                heapq.heappush(heap, (record[0], index, record[1], record))422        while heap:423            _, index, _, record = heapq.heappop(heap)424            accumulator.consume(record)425            following = next(streams[index], None)426            if following is not None:427                heapq.heappush(heap, (following[0], index, following[1], following))428        for state in self.files:429            if state['consumed'] != state['produced']:430                raise ValueError('归并结束后仍缺少贡献块')431            accumulator.finish_file(state['eof'])432        self.stats['main_merge_and_consume_seconds'] += perf_counter() - started433434435    def close(self, success=False):436        """先结束实际写入者,再关闭句柄和自有费用;失败槽保留到后续重试。437438        参数: success要求正常退出。返回: None。异常: 原取消或汇总关闭错误;439        不删除任何暂存目录,未关闭资源仍由本池持有。440        """441        if self._closed:442            return443        self._closing = True444        errors = []445        if not success:446            try:447                self.stop.set()448            except BaseException as error:449                errors.append(error)450        for slot in tuple(self.slots):451            process = slot['process']452            try:453                if process is not None and not getattr(process, '_closed', False) and process.pid is not None:454                    if success and not slot['stop_requested']:455                        slot['control'].send(('close',))456                        slot['stop_requested'] = True457                        process.join(5)458                    if process.is_alive():459                        process.terminate()460                        process.join(5)461                        if success:462                            raise RuntimeError('fast工作进程未正常关闭')463                    if process.is_alive():464                        raise RuntimeError('fast工作进程仍在运行')465                    if success and process.exitcode != 0:466                        raise RuntimeError('fast工作进程关闭失败')467            except BaseException as error:468                errors.append(error)469            tree = slot['tree']470            if tree is not None:471                try:472                    tree.close()473                except BaseException as error:474                    errors.append(error)475                finally:476                    # 构造失败可能只剩OpenProcess句柄,不能只检查作业_handle。477                    if tree._closed:478                        slot['tree'] = None479            if process is not None:480                try:481                    if not getattr(process, '_closed', False):482                        if process.pid is not None:483                            process.join(0)484                            if process.is_alive():485                                raise RuntimeError('fast写入进程未结束,保留资源责任')486                        process.close()487                except BaseException as error:488                    errors.append(error)489                finally:490                    if getattr(process, '_closed', False):491                        slot['process'] = None492            for key in ('control', 'data', 'child_control', 'child_data', 'startup'):493                connection = slot.get(key)494                if connection is not None:495                    try:496                        connection.close()497                    except BaseException as error:498                        errors.append(error)499                    finally:500                        if connection.closed:501                            slot[key] = None502            if all(slot.get(key) is None for key in ('process', 'tree', 'control', 'data', 'child_control', 'child_data', 'startup')):503                self.slots.remove(slot)504        if not self.slots:505            try:506                _release_owned_ram(self, self.worker_cost, 'worker_cost')507            except BaseException as error:508                errors.append(error)509        self._closed = not self.slots and self._held_cost == 0510        if self._closed:511            # 所有子进程已结束后解除本池对自有同步事件的引用;不改调用方的cancel。512            self.stop = None513        if errors:514            first = errors[0]515            for other in errors[1:]:516                first.add_note('fast工作池另有清理失败: ' + type(other).__name__)517            if isinstance(first, (KeyboardInterrupt, SystemExit)):518                raise first519            raise RuntimeError('fast进程资源关闭失败') from first520521522523class _SessionResources:524    """仅管理一个fast会话的固定费用、读取器、工作池和自有目录。"""525526    def __init__(self, budget):527        """建立空责任槽。参数: 本次账本。返回: 新对象。异常: 无资源创建。"""528        self._budget, self._held_cost, self._fixed_cost = budget, 0, 0529        self.reader = self.pool = self.directory = self.directory_identity = None530        self._closed = False531532    def open_reader(self, spec, database, cancel):533        """创建并登记本会话读取器,收编构造中未能关闭的真实子所有者。534535        参数: 文件声明、DBC、取消。返回: FileContributions。异常: 原构造错误。536        ASC/BLF构造失败均收编实际未关闭的来源owner,直到成功关闭才清槽。537        """538        if self.reader is not None:539            raise RuntimeError('fast前一读取器尚未关闭')540        try:541            self.reader = FileContributions(542                spec, database, cancel, self._budget, _programs_checked=True)543            return self.reader544        except BaseException as error:545            pending = getattr(error, 'fast_reader_cleanup_owner', None)546            if pending is None:547                pending = getattr(error, 'asc_read_cleanup_owner', None)548            if pending is None:549                pending = getattr(error, 'blf_read_cleanup_owner', None)550            self.reader = pending551            raise552553    def close_reader(self):554        """关闭实际读取器;仅在真实关闭完成后清槽。参数: 无。返回: None。异常: 原关闭错误。"""555        reader = self.reader556        if reader is None:557            return558        try:559            reader.close()560            if not getattr(reader, 'closed', False):561                raise RuntimeError('fast读取器close返回但未完成关闭')562        finally:563            if getattr(reader, 'closed', False):564                self.reader = None565566    def create_directory(self, work_dir):567        """独占创建并登记目录,避免自动终结器在写入者未结束时删除目录。568569        参数: 可选工作父目录。返回: Path。异常: 创建或身份登记错误,已有目录不删除。570        """571        from ..results.owned_directory import directory_identity572        if work_dir is not None:573            Path(work_dir).mkdir(parents=True, exist_ok=True)574        self.directory = Path(mkdtemp(prefix='fast-session-', dir=work_dir))575        self.directory_identity = directory_identity(self.directory)576        return self.directory577578    def close(self):579        """依次停止工作池、读取器、删除自有目录并退款,任一步失败保留重试责任。580581        参数: 无。返回: None。异常: 原清理错误;未确认停止写入者时不删除目录。582        """583        if self._closed:584            return585        try:586            pool = self.pool587            if pool is not None:588                try:589                    pool.close()590                    if not pool._closed:591                        raise RuntimeError('fast工作池close返回但未完成关闭')592                finally:593                    if pool._closed:594                        self.pool = None595            self.close_reader()596            if self.directory is not None:597                from ..results.owned_directory import remove_owned_directory598                remove_owned_directory(self.directory, self.directory_identity)599                self.directory = self.directory_identity = None600            _release_owned_ram(self, self._fixed_cost, '_fixed_cost')601        finally:602            self._closed = (self.pool is None and self.reader is None603                            and self.directory is None and self._held_cost == 0)604605606def run_session(spec, database=None, *, work_dir=None, cancel_event=None, statistics=None,607                _programs_checked=False):608    """运行一个连续fast任务。参数: 编译spec、可复用DBC、临时目录、取消事件、可选统计dict。609    返回: (业务行list, 提示list)。异常: 配置/读取/公式/资源/协议/取消;不返回部分结果。610    _programs_checked由公共编译后的内部入口传入;直接调用默认仍检查文本和指令。611    """612    import cantools613    spec = effective_spec(spec)614    # 原生低层入口仍完整检查文本;同一会话后续文件只绑定已经核过的指令。615    # 公共编译器调用链显式传入准备状态,不信任 spec 中的同名字段。616    specs = _checked_file_specs(spec, _programs_checked)617    count = _worker_process_limit(spec['process_count'], len(specs))618    budget = ResourceBudget(spec.get('memory_budget_bytes', 268435456), spec.get('spool_quota_bytes'))619    stats = statistics if statistics is not None else {}620    stats.update(worker_pids=[], files=[], received_bytes=0, spool_written_bytes=0,621                 spool_live_bytes=0, spool_peak_bytes=0, prefetch_pauses=0,622                 main_wait_seconds=0.0, main_spool_read_seconds=0.0,623                 main_decode_seconds=0.0, main_merge_and_consume_seconds=0.0,624                 detailed_profile=spec.get('_profile', False))625    # 覆盖计划/累计器、内联读取空间与归并各流当前块;不是整个进程RSS承诺。626    cost = RAM * (2 + (len(specs) if spec.get('file_merge') == 'time_order' else 1))627    cost += len(json.dumps({k:v for k,v in spec.items() if k != '_database'}).encode()) * 32628    if count == 0 and all(Path(item['path']).suffix.lower() == '.blf' for item in specs):629        # 内联读取逐文件开关,最多一个BLF owner;从原固定费移交64KiB,避免重复计费。630        # 混合来源/工作进程的既有分摊不变,来源owner仍预约自身实际费用。631        cost -= 65536632    resources = _SessionResources(budget)633    accumulator = primary = None634    try:635        # 首次申请和累计器构造均在责任保护区内,申请完成后取消也有真实费用槽。636        _reserve_owned_ram(resources, cost, '_fixed_cost')637        accumulator = SessionAccumulator(spec)638        if cancel_event is not None and cancel_event.is_set():639            raise InterruptedError('fast会话启动前取消')640        if '_dbc_digest' in spec and sha256(Path(spec['dbc']).read_bytes()).hexdigest() != spec['_dbc_digest']:641            raise ValueError('编译后DBC内容变化')642        if database is None:643            database = load_dbc_file(spec['dbc'], cantools)644        if count == 0 and (len(specs) == 1 or spec.get('file_merge', 'concatenate') == 'concatenate'):645            for item in specs:646                reader = resources.open_reader(item, database, cancel_event)647                for record in reader:648                    accumulator.consume(record)649                accumulator.finish_file(reader.summary())650                if spec.get('_profile', False):651                    stats['files'].append(dict(path=item['path'], **reader.timings))652                resources.close_reader()653        else:654            directory = resources.create_directory(work_dir)655            pool = FilePool(specs, budget, directory, cancel_event, stats, _programs_checked=True)656            resources.pool = pool657            if count == 0:658                # 内联时间归并仍有必要暂存;读取器和目录均归当前资源所有者。659                for index, item in enumerate(specs):660                    reader = resources.open_reader(item, database, cancel_event)661                    number = 0662                    for records in batches(reader):663                        packet = encode(index, number, records)664                        budget.reserve_spool(len(packet))665                        (directory / f'{index}-{number}.block').write_bytes(packet)666                        stats['spool_written_bytes'] += len(packet)667                        stats['spool_live_bytes'] += len(packet)668                        stats['spool_peak_bytes'] = max(stats['spool_peak_bytes'], stats['spool_live_bytes'])669                        number += 1670                    pool.files[index].update(produced=number, spool=number, eof=reader.summary())671                    resources.close_reader()672            else:673                pool.start(count)674            if pool.ordered:675                pool.time_order(accumulator)676            else:677                pool.concatenate(accumulator)678            pool.close(True)679        result = accumulator.result()680        stats['raw_record_count'] = accumulator.raw681        stats['calculation_counts'] = [dict(name=metric.name, targets=accumulator.targets[i],682            valid=accumulator.valid[i], selected=accumulator.selected[i]) for i, metric in enumerate(accumulator.metrics)]683        resources.close()684        return result685    except BaseException as error:686        primary = error687        _cleanup_owner(resources, error, 'fast_session_cleanup_owner', 'fast会话')688        raise689    finally:690        try:691            stats.update(budget.snapshot())692            if accumulator is not None and accumulator.profile:693                stats['main_accumulate_seconds'] = accumulator.accumulate_seconds694        except BaseException as diagnostic:695            if primary is None:696                raise697            primary.add_note('fast资源统计附加失败: ' + type(diagnostic).__name__)698
Chronaxis 用户手册 · 离线阅读