Chronaxis用户手册English
本页目录

chronaxis.execution.csv_reader

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在独立信号CSV读取、引用记录累积和数组分配前申请共享预算。23输入沿用SignalCSVSpec、声明及ReadRequest;输出沿用原SignalBatch和CSVSourceRef。4原csv.reader、解析和文件顺序保持;批次达到样本上限或剩余容量前交付。5物理行readline限制按当时剩余RAM计算,跨物理行的引用记录保守累计,超限失败。6原样本/ref、定宽Unicode提升、owned数组交接与协议pending副本分别计账。7close释放本来源/请求引用和预算,文件保留;不是全进程RSS或外部保留全部输出保证。8默认请求各自保存真实生成器、FD与用途费用槽,关闭取消后保留尚未完成的责任;9未返回构造以csv_source_cleanup_owner、csv_request_cleanup_owner或csv_resources_cleanup_owner恢复。10"""1112# 新来源仍使用既有CSV解释和数值归一化,资源模型不依赖旧CAN链。13from collections.abc import Iterator14from sys import getsizeof, int_info15from ..results.budget import ResourceBudget16from ..results.errors import BudgetExceededError17from ..results.spool import _cleanup_owner, _reserve_owned_ram, _release_owned_ram18from ..sources.signal_csv import SignalCSVSourceAdapter19from ..sources.protocol import SourceClosedError, SourceReadError20from ..sources.models import ReadRequest, SignalBatch21from ..sources.csv_spec import SignalCSVSpec22from ..core.binding import SignalDefinition23from .history import _metadata_bytes242526def _expanded_integer_bytes(text):27    """按十进制文本估算整数展开空间,不重复执行 Decimal 或实际整数转换。2829    参数: text 为待解析单元格。返回: 最多 4096 位十进制整数所需的字节上界。30    异常: 不把坏文本提前当成合法数值;语法和指数边界仍由原解析器拒绝。31    """32    # 普通短整数由记录的固定标量留量覆盖;仅指数或长数值需要额外空间。33    exponent_at = max(text.find('e'), text.find('E'))34    if exponent_at < 0 and len(text) <= 20:35        return 3236    digits = len(text) if exponent_at < 0 else exponent_at37    if exponent_at >= 0:38        exponent = text[exponent_at + 1:].strip()39        if not exponent.startswith('-'):40            exponent = exponent.lstrip('+').lstrip('0')41            if exponent and exponent.isdecimal():42                # 原解析器最多允许 4096 位;不为估算构造任意长度的指数整数。43                digits += int(exponent) if len(exponent) <= 4 else 409644    digits = min(4096, max(1, digits))45    # log2(10) < 3.322;按本 Python 的 PyLong 数字位宽计算,不依赖 float 舍入。46    limbs = (digits * 3322 + int_info.bits_per_digit * 1000 - 1) // (int_info.bits_per_digit * 1000)47    return getsizeof(0) + max(0, limbs - 1) * int_info.sizeof_digit484950class BudgetedSignalCSVSourceAdapter(SignalCSVSourceAdapter):51    """为各请求独立持有检查器,固定声明一次计账,关闭不删除输入。5253    _held_cost只含本来源_input_cost;请求与资源各自拥有自己的费用。54    _closing拒绝继续读取,只有真实子资源及固定槽均已清完才closed。55    未安装请求和检查器保留在明确字段,不借共享余额差额补账。56    """5758    def __init__(self, budget: ResourceBudget, spec: SignalCSVSpec, session_id: str,59                 signals: tuple[SignalDefinition, ...], *, backend: str = 'numpy', clock_only=(), retain_original=True, consumer_sample_bytes=0):60        """先计声明,再建立不读取文件的原CSV适配器。6162        参数: budget为共享RAM账本;其余沿用原CSV入口,backend默认numpy。63            clock_only只取时间;retain_original=False省去逐行原文引用,默认保留旧入口行为。64            consumer_sample_bytes为接收方每样本最坏排队费用,只用于容量协调,不重复预约。65        返回: 新预算CSV来源。异常: 类型/配置/预算失败;持续清理失败附csv_source_cleanup_owner。66        """67        if not isinstance(budget, ResourceBudget) or not isinstance(spec, SignalCSVSpec) or type(signals) is not tuple:68            raise TypeError('预算CSV需要ResourceBudget/SignalCSVSpec/信号tuple')69        if type(consumer_sample_bytes) is not int or consumer_sample_bytes < 0:70            raise ValueError('CSV接收方样本费用须为非负整数')71        self._consumer_sample_bytes = consumer_sample_bytes72        # 路径/元数据和choices固定容器在原适配器构造前保守计数。73        # Path/tzinfo不是结果层可序列化模型;路径计原文本,时区只保留原对象引用。74        declaration = (spec.name, spec.timebase,75                       spec.time_column, spec.unit, spec.offset_ns, spec.encoding, spec.delimiter,76                       spec.missing_values, session_id, signals)77        # 逐路径计数避免在申请前再复制完整路径tuple;每次只持一个路径文本。78        cost = 20480 + _metadata_bytes(declaration) * 16 + sum(512 + _metadata_bytes(str(path)) * 16 for path in spec.paths)79        # 首次预算回调前建立可关闭的真实空壳;父构造尚未返回也不依赖未初始化字段。80        self.budget = self._budget = budget81        self._input_cost = self._held_cost = 082        self.closed = self._closing = False83        self.spec, self.session_id, self.signals, self.backend, self.choices = spec, session_id, signals, backend, {}84        self.active, self.handles = set(), set()85        self._requests, self._resources = set(), set()86        self._uninstalled_request = self._uninstalled_resources = None87        try:88            _reserve_owned_ram(self, cost, '_input_cost')89            super().__init__(spec, session_id, signals, backend=backend,clock_only=clock_only,retain_original=retain_original)90        except BaseException as exc:91            _cleanup_owner(self, exc, 'csv_source_cleanup_owner', '预算CSV来源构造')92            raise9394    def iter_batches(self, request: ReadRequest, *, observe_bounds=False) -> Iterator[SignalBatch]:95        """预留请求/validator固定结构,再建立原参数生成器。9697        参数: request为ReadRequest,投影/会话由原入口验证;observe_bounds仅收计算所需首末时刻。98        返回: 独立预算游标,不生成额外观测报告。99        异常: 关闭/配置/预算/读取错误传播;未返回请求持续清理失败保最高恢复owner,100            不切换后端;batch_rows 是上限,容量不足时提前交付已有样本。101        """102        self._ensure_open()103        # 只限制创建新请求;已有其他活read仍可推进,不能覆盖尚未返回的最高子责任。104        if self._uninstalled_request is not None or self._uninstalled_resources is not None:105            raise SourceClosedError('预算CSV来源尚有未返回请求或资源待清理')106        try:107            iterator = _CSVRequest(self, request, observe_bounds=observe_bounds)108            self._uninstalled_request = iterator109            self._requests.add(iterator)110            self._uninstalled_request = None111        except BaseException as exc:112            iterator = self._uninstalled_request or getattr(exc, 'csv_request_cleanup_owner', None)113            if iterator is not None:114                _cleanup_owner(iterator, exc, 'csv_request_cleanup_owner', '默认CSV请求登记')115                if iterator.closed or iterator in self._requests:116                    self._uninstalled_request = None117                else:118                    self._uninstalled_request = iterator119                if not iterator.closed:120                    exc.csv_source_cleanup_owner = self121            raise122        return iterator123124    def iter_batches_with_observation(self, request: ReadRequest):125        """创建本次独立读取owner,完整耗尽并清理成功后才开放原时间行观测事实。126127        参数: request为原ReadRequest,不改变会话、投影、范围或batch_rows。128        返回: CSVObservationRead;迭代交付原SignalBatch,真正StopIteration后129            observation给出本read独立的首末时间、原行数、请求及读取身份。130        异常: 原配置、预算、读取或取消错误;持续构造清理失败时从原异常的131            csv_observation_read_cleanup_owner恢复close。132        副作用: 增加本read自有固定预算,默认iter_batches不启用此采集。133        """134        # 延迟导入避免预算来源与它的可选资源子类循环;默认入口不导入或构造新owner。135        from .csv_observation import CSVObservationRead136        from ..results.spool import _cleanup_owner137        self._ensure_open()138        # 可选观测同样不能越过默认路径遗留的未返回责任,但不改变其他read的开放检查。139        if self._uninstalled_request is not None or self._uninstalled_resources is not None:140            raise SourceClosedError('预算CSV来源尚有未返回请求或资源待清理')141        read = CSVObservationRead(self, request)142        try:143            # 每个read按对象登记,同一request可独立同时扫描,不用request值作全局key。144            self._requests.add(read)145        except BaseException as exc:146            # 未返回的登记失败也不是可恢复推进状态,最高read只保留关闭责任。147            read._failed = True148            _cleanup_owner(read, exc, 'csv_observation_read_cleanup_owner', 'CSV观测读取登记')149            raise150        return read151152    def _ensure_open(self) -> None:153        """在固定声明已释放后仍抛明确关闭错误,不访问已丢弃spec。154155        参数: 无。返回: None。异常: SourceClosedError为来源已关闭。156        """157        if self.closed or self._closing:158            raise SourceClosedError('预算CSV来源已关闭')159160    def _read_resources(self, request: ReadRequest) -> '_CSVResources':161        """在原迭代器实际打开文件前建立独立资源检查器。162163        参数: request为已校验请求。返回: _CSVResources。164        异常: 固定预算不足或登记失败;持续清理失败保实际检查器及最高来源恢复owner。165        """166        # 此入口只管理未被请求owner接管的原资源,禁止覆盖已有未安装检查器。167        if self._uninstalled_resources is not None:168            raise SourceClosedError('预算CSV来源尚有未返回资源待清理')169        try:170            resources = _CSVResources(self)171            self._uninstalled_resources = resources172            self._resources.add(resources)173            self._uninstalled_resources = None174        except BaseException as exc:175            resources = self._uninstalled_resources or getattr(exc, 'csv_resources_cleanup_owner', None)176            if resources is not None:177                _cleanup_owner(resources, exc, 'csv_resources_cleanup_owner', '默认CSV资源登记')178                self._uninstalled_resources = None if resources.closed or resources in self._resources else resources179                if not resources.closed:180                    exc.csv_source_cleanup_owner = self181            raise182        return resources183184    def _close_handle(self, handle, primary):185        """仅在真实FD关闭后移除登记,业务根因不被附加清理错误覆盖。186187        参数: handle实际文件;primary已有原错或None。返回: None。188        异常: 无主错或GeneratorExit时原关闭错误传播;其他主错只增加note。189        """190        try:191            handle.close()192        except BaseException as cleanup:193            if primary is None or isinstance(primary, GeneratorExit):194                raise195            primary.add_note(f'预算CSV文件关闭附加失败: {type(cleanup).__name__}')196        finally:197            if handle.closed:198                self.handles.discard(handle)199200    def close(self) -> None:201        """关闭全部包装请求、原句柄与检查器,最后抵销固定声明预算。202203        参数: 无。返回: None,成功后幂等。异常: 首个关闭错误,仍尝试其他独立请求;204            尚未结束的子责任及费用保留,恢复后可重复close。205        """206        if self.closed:207            return208        self._closing = True209        first_error = None210        # 未登记子对象仍由本来源拥有;已在集合中的同一对象不会再保留第二个未安装槽。211        if self._uninstalled_request is not None:212            try:213                self._uninstalled_request.close()214                self._uninstalled_request = None215            except BaseException as exc:216                first_error = exc217        # 一个生成器关闭失败也不能跳过其他独立请求及原句柄。218        for iterator in tuple(self._requests):219            try:220                iterator.close()221            except BaseException as exc:222                if first_error is None:223                    first_error = exc224                else:225                    first_error.add_note(f'其他预算CSV请求清理失败: {type(exc).__name__}')226        # 活请求已经负责其生成器/FD/资源;这里只关闭没有更高请求owner的残留资源。227        requests = tuple(self._requests) + (() if self._uninstalled_request is None else (self._uninstalled_request,))228        for iterator in tuple(self.active):229            if any(getattr(request, 'iterator', getattr(request, '_iterator', None)) is iterator for request in requests):230                continue231            try:232                iterator.close()233                self.active.discard(iterator)234            except BaseException as exc:235                if first_error is None:236                    first_error = exc237        for handle in tuple(self.handles):238            if any(getattr(request, '_handle', None) is handle for request in requests):239                continue240            try:241                self._close_handle(handle, None)242            except BaseException as exc:243                if first_error is None:244                    first_error = exc245        resources_to_close = tuple(self._resources) + (() if self._uninstalled_resources is None else (self._uninstalled_resources,))246        for resources in resources_to_close:247            if any(getattr(request, '_resources', None) is resources for request in requests):248                continue249            try:250                resources.close()251                if resources is self._uninstalled_resources:252                    self._uninstalled_resources = None253            except BaseException as exc:254                if first_error is None:255                    first_error = exc256        children_closed = (not self._requests and not self._resources and not self.active and not self.handles257            and self._uninstalled_request is None and self._uninstalled_resources is None)258        if children_closed:259            try:260                _release_owned_ram(self, self._input_cost, '_input_cost')261            except BaseException as exc:262                if first_error is None:263                    first_error = exc264            finally:265                if self._input_cost == 0:266                    self.spec, self.signals, self.choices = None, (), {}267                    self.closed = True268        if first_error is not None:269            if not self.closed:270                first_error.csv_source_cleanup_owner = self271            raise first_error272273274class _CSVRequest:275    """拥有默认原生成器、真实FD和资源检查器,半关闭只允许重试close。"""276277    def __init__(self, owner, request, *, observe_bounds=False):278        """先预约原固定请求费用,再接住原生成器,未返回失败保实际恢复owner。279280        参数: owner为预算CSV来源;request沿原结构;observe_bounds按需保存两个时间整数。返回: 独立默认请求。281        异常: 原配置、预算、取消或登记错误;持续清理失败附csv_request_cleanup_owner。282        """283        if type(observe_bounds) is not bool:284            raise TypeError("observe_bounds需要bool")285        self.observe_bounds=observe_bounds286        self.observed_start=self.observed_end=None287        self.owner, self._budget = owner, owner.budget288        self.closed = self._closing = False289        self.current = self.iterator = self._resources = self._handle = None290        self.cost = self._held_cost = 0291        try:292            # 金额与旧请求完全一致;resource_sink只接资源,不采集首末原行事实。293            _reserve_owned_ram(self, 16384 + len(owner.signals) * 4096, 'cost')294            self.iterator = SignalCSVSourceAdapter._request_batches(owner, request, resource_sink=self)295        except BaseException as exc:296            self._adopt_cleanup(exc)297            _cleanup_owner(self, exc, 'csv_request_cleanup_owner', '默认CSV请求构造')298            raise299300    def _adopt_cleanup(self, exc):301        """接住本次尚未返回的检查器,不接他请求或借用来源。302303        参数: exc原异常。返回: None。异常: 无预期异常。304        """305        resources = getattr(exc, 'csv_resources_cleanup_owner', None)306        if resources is not None and self._resources is None:307            self._resources = resources308309    def _created_iterator(self, iterator):310        """在原活动登记前接住同一个实际生成器。311312        参数: iterator原生成器。返回: None。异常: 无预期异常。313        """314        self.iterator = iterator315316    def _read_resources(self):317        """建立本请求唯一检查器,登记前失败仍由本请求保留责任。318319        参数: 无。返回: 真实_CSVResources。异常: 原预算、构造或集合登记错误。320        """321        # 同一次原生成器只建立一个检查器;失败后的旧责任须先由同owner关闭。322        if self._resources is not None:323            raise SourceClosedError('默认CSV请求尚有资源待清理')324        try:325            self._resources = _CSVResources(self.owner)326            self.owner._resources.add(self._resources)327        except BaseException as exc:328            self._adopt_cleanup(exc)329            raise330        return self._resources331332    def _opened_handle(self, handle):333        """在原handles集合登记前接住真实只读FD。334335        参数: handle实际文件。返回: None。异常: 无预期异常。336        """337        self._handle = handle338339    def _close_handle(self, handle, primary):340        """执行原文件关闭责任,失败时保持本请求的实际FD引用。341342        参数: handle实际FD;primary已有业务原错或None。返回: None。343        异常: 无主错或GeneratorExit时传播原关闭错误,其余主错保留并加note。344        """345        try:346            self.owner._close_handle(handle, primary)347        finally:348            if handle.closed:349                self._handle = None350351    def _end_file(self, resources, primary):352        """抵销文件局部费用,已有业务原错或取消不被附加释放错误取代。353354        参数: resources本请求检查器;primary为生成器现场原错。355        返回: None。异常: 正常结束或GeneratorExit时传播原清理错误。356        """357        try:358            resources.end_file()359        except BaseException as cleanup:360            if primary is None or isinstance(primary, GeneratorExit):361                raise362            primary.add_note(f'默认CSV文件费用清理附加失败: {type(cleanup).__name__}')363364    def _close_resources(self, primary):365        """仅在真实FD已关闭后清理本请求检查器,保留原主异常。366367        参数: primary原错或None。返回: None。异常: 正常关闭失败原样传播。368        """369        if self._handle is not None:370            if primary is None:371                raise SourceReadError('默认CSV文件尚未成功关闭')372            return373        try:374            self._resources.close()375        except BaseException as cleanup:376            if primary is None or isinstance(primary, GeneratorExit):377                raise378            primary.add_note(f'默认CSV资源关闭附加失败: {type(cleanup).__name__}')379380    def __iter__(self):381        """返回自有游标。参数: 无。返回: self。异常: 无。"""382        return self383384    def __next__(self) -> SignalBatch:385        """推进原生成器,失败/EOF只清自有资源并保留原异常和最高恢复owner。386387        参数: 无。返回: 原SignalBatch。异常: 原关闭、EOF、预算、读取或取消错误。388        """389        self.owner._ensure_open()390        if self.closed:391            raise StopIteration392        if self._closing:393            raise SourceClosedError('默认CSV请求正在清理')394        # 结束上一批借引用,原生成器及检查器仍持有原费用直到原交接完成。395        self.current = None396        try:397            self.current = next(self.iterator)398            return self.current399        except StopIteration:400            # 真实EOF尚须本请求固定费用关闭成功;此时没有业务主错,不能用StopIteration遮蔽清理取消。401            self.close()402            raise403        except BaseException as exc:404            self._adopt_cleanup(exc)405            _cleanup_owner(self, exc, 'csv_request_cleanup_owner', '默认CSV请求推进')406            raise407408    def close(self) -> None:409        """依次关闭真实生成器/FD/检查器,最后抵销固定费用,成功后才closed。410411        参数: 无。返回: None,幂等。异常: 原关闭或预算错误;尚未清完附最高请求owner。412        """413        if self.closed:414            self.owner._requests.discard(self)415            return416        self._closing = True417        first_error = None418        # 生成器可能已在第一次close中退出finally,FD与检查器字段仍须独立重试。419        if self.iterator is not None:420            try:421                self.iterator.close()422            except BaseException as exc:423                first_error = exc424        if self._handle is not None:425            try:426                self.owner._close_handle(self._handle, None)427            except BaseException as exc:428                if first_error is None:429                    first_error = exc430            finally:431                if self._handle.closed:432                    self._handle = None433        if self._handle is None and self._resources is not None:434            try:435                self._resources.close()436            except BaseException as exc:437                if first_error is None:438                    first_error = exc439            finally:440                if self._resources.closed:441                    self._resources = None442        # 实际生成器已关闭且所有子责任结束才释放其固定请求槽;不抵销借用adapter费用。443        iterator_closed = self.iterator is None or self.iterator.gi_frame is None444        if iterator_closed and self._handle is None and self._resources is None:445            self.owner.active.discard(self.iterator)446            self.current = None447            try:448                _release_owned_ram(self, self.cost, 'cost')449            except BaseException as exc:450                if first_error is None:451                    first_error = exc452            finally:453                if self.cost == 0:454                    self.closed = True455                    self.iterator = None456                    self.owner._requests.discard(self)457        if first_error is not None:458            if not self.closed:459                first_error.csv_request_cleanup_owner = self460            raise first_error461462463class _CSVResources:464    """每请求按用途保存金额,固定读取缓冲、行/ref、数组和协议状态共同计账。465466    八个实际属性槽是付款依据,charges只提供兼容视图;_held_cost为槽位总和。467    _closing后不再增加费用,所有用途真实抵销完成才closed并解除来源登记。468    """469470    def __init__(self, owner):471        """预留文件解码缓冲及资源容器,不在申请前打开文件。472473        参数: owner为预算CSV来源。返回: 独立检查器。异常: BudgetExceededError。474        """475        self.owner, self.budget, self.closed = owner, owner.budget, False476        self._budget, self._held_cost, self._closing = owner.budget, 0, False477        self._fixed_cost = self._reading_cost = self._header_cost = self._row_cost = 0478        self._pending_cost = self._work_cost = self._array_cost = self._protocol_cost = 0479        self.charges, self.line_source, self.array_cost = {}, None, 0480        self._sample_count, self._column_count = 0, {}481        self._string_layout = {}482        # TextIO解码缓冲和固定容器保守64KiB,不声称整进程RSS。483        try:484            self._add('fixed', 65536)485        except BaseException as exc:486            # 可选观测子类已有同一真实恢复边界,默认构造只处理自己的未返回资源。487            if type(self) is _CSVResources:488                _cleanup_owner(self, exc, 'csv_resources_cleanup_owner', '默认CSV资源构造')489            raise490491    def _sync(self, key):492        """把真实付款槽同步到兼容charges视图,不从视图反推所有权。493494        参数: key为八个固定用途之一。返回: None。异常: 原字典登记错误可重试。495        """496        amount = getattr(self, '_' + key + '_cost')497        if amount:498            self.charges[key] = amount499        else:500            self.charges.pop(key, None)501502    def _add(self, key, amount):503        """事务式登记一次真实预约,未入账超限不增加自有金额。504505        参数: key为用途;amount为bytes。返回: None。异常: 账本超限/金额错误/取消;506            实际已入账而返回取消时仍保留真实付款槽。507        """508        if self.closed or self._closing:509            raise SourceClosedError('默认CSV资源已关闭或正在清理')510        try:511            _reserve_owned_ram(self, amount, '_' + key + '_cost')512        finally:513            self._sync(key)514515    def _release_amount(self, key, amount):516        """只抵销本用途真实费用,释放前后取消均保留正确槽位。517518        参数: key固定用途;amount不超过本槽bytes。返回: None。异常: 原释放或取消。519        """520        try:521            _release_owned_ram(self, amount, '_' + key + '_cost')522        finally:523            self._sync(key)524525    def _move(self, source, target, amount):526        """转移已付款责任槽,总费用不变,不申请或归还共享余额。527528        参数: source/target固定用途,amount为真实已有bytes。返回: None。异常: 金额矛盾ValueError。529        """530        source_slot, target_slot = '_' + source + '_cost', '_' + target + '_cost'531        if amount < 0 or amount > getattr(self, source_slot):532            raise ValueError('默认CSV费用交接超出原用途余额')533        setattr(self, source_slot, getattr(self, source_slot) - amount)534        setattr(self, target_slot, getattr(self, target_slot) + amount)535        self._sync(source)536        self._sync(target)537538    def _release(self, key):539        """抵销某用途全部预留。参数: key用途。返回: None。异常: 账本一致性错误。"""540        self._release_amount(key, getattr(self, '_' + key + '_cost'))541542    def lines(self, handle):543        """把真实TextIO交给限长物理行迭代器,不自己解释CSV。544545        参数: handle为原适配器只读文件。返回: _PhysicalLines。异常: 构造错误传播。546        """547        self.line_source = _PhysicalLines(self, handle)548        return self.line_source549550    def records(self, reader):551        """保持原reader的物理计数和解释,为表头/当前逻辑记录保存累积预算。552553        参数: reader为原csv.reader。返回: _CSVRecords。异常: 构造错误传播。554        """555        return _CSVRecords(self, reader)556557    def _sample_estimate(self, time_text, text, definition):558        """估算新记录独有的容器、数值和文本,不重复计算来源路径或声明。559560        参数: time_text/text 为原单元格;definition 为当前信号声明。561        返回: (记录费用, 字符串原值长度),长度为零表示没有定宽字符串列。562        异常: 输入已由读取器限定;声明查找错误直接传播。563        """564        # 四项 tuple、list 槽、时间/序号/原值和 CSVSourceRef 的实例/字典留量。565        # 路径、字段、会话和来源名均引用固定声明;不按每条样本重新递归收费。566        cost = 384 + getsizeof(text) + getsizeof(time_text)567        width = 0568        if text not in self.owner.spec.missing_values:569            choices = self.owner.choices[definition.name]570            if text in choices:571                original = choices[text]572                width = len(original) if type(original) is str else 0573                if type(original) is int:574                    cost += max(0, getsizeof(original) - 32)575            elif any(type(code) is str for code, _ in definition.choices):576                width = len(text)577            else:578                # 例如 1e4095 只有六个字符,却会在原解析器中展开为 1840B 整数。579                cost += max(0, _expanded_integer_bytes(text) - 32)580        return cost, width581582    def _pack_estimate(self, validator, count, columns, unicode_bytes):583        """分别计算最终列、归一化工作区和跨批同刻检查状态的峰值。584585        参数: validator 为现有协议状态,其余为候选批大小、列数和字节估算。586        返回: (保留列费用, 打包峰值费用)。异常: 无预期异常。587        """588        # 输出五个数组每项至多 27B,附原码列、引用 tuple 和数组对象留量。589        # 输出仍引用的原值/CSVSourceRef/文本已在 pending 付费,交付时转交 array,590        # 不在临时工作区再付第二次;这里只预留新增数组、指针 tuple 和模型对象。591        retained = 4096 + columns * 1024 + count * 64592        # 384B/样本覆盖类型分组 tuple/索引、临时 records、原值指针、数值数组及掩码。593        # Unicode 仅归一化时存在一份原始 UTF32 数组,不是八份长期输出。594        identities = sum(len(state[1]) for state in validator.pending.values())595        state_cost = 4096 + len(self.owner.signals) * 512 + identities * 256 + count * 256596        workspace = retained + count * 384 + unicode_bytes + state_cost597        return retained, workspace598599    def should_flush(self, time_text, text, definition, validator):600        """在再接收一条样本之前判断是否应先交付当前批。601602        参数: 原文本、声明和当前协议状态;不解析值、不修改缓冲或预算。603        返回: bool;已有样本且继续累积会挤占打包空间时为 True。604        异常: 无预期异常;单条无法容纳由实际预约明确失败。605        """606        if not self._sample_count:607            return False608        cost, width = self._sample_estimate(time_text, text, definition)609        columns = len(self._column_count) + (definition.name not in self._column_count)610        old_count, old_width = self._string_layout.get(definition.name, (0, 0))611        unicode_bytes = max((n * w * 4 for name, (n, w) in self._string_layout.items()612                             if name != definition.name), default=0)613        if width:614            unicode_bytes = max(unicode_bytes, (old_count + 1) * max(old_width, width) * 4)615        else:616            unicode_bytes = max(unicode_bytes, old_count * old_width * 4)617        if self.owner.backend != 'numpy':618            unicode_bytes = 0619        _, workspace = self._pack_estimate(validator, self._sample_count + 1, columns,620                                           unicode_bytes)621        # 为下一物理行和当前解析结果交接保留 64KiB;不修改用户的 batch_rows。622        available = self.budget.memory_budget_bytes - self.budget.ram_bytes623        receiver_peak = (self._sample_count + 1) * self.owner._consumer_sample_bytes624        # 有接收队列时,输入打包与队列最多使用当前余量的一半,另一半供正在625        # 计算的积分/统计申请临时数组。此处只决定批大小,不把余量重复登记为费用。626        capacity = available // 2 if self.owner._consumer_sample_bytes else available627        return cost + workspace + receiver_peak + 65536 > capacity628629    def before_sample(self, time_text, text, path, definition):630        """在值解析及原记录 append 前预留记录独有的数据。631632        参数: 原单元格、实际路径及信号声明;路径已在来源构造时计费。633        返回: None。异常: 预算超限或预约取消,仍保留真实已付金额。634        """635        cost, width = self._sample_estimate(time_text, text, definition)636        self._add('pending', cost)637        self._sample_count += 1638        self._column_count[definition.name] = self._column_count.get(definition.name, 0) + 1639        if width:640            count, maximum = self._string_layout.get(definition.name, (0, 0))641            self._string_layout[definition.name] = (count + 1, max(maximum, width))642643    def before_pack(self, pending, validator):644        """在创建数组前预留实际候选批的峰值,不按最大批大小预先拒绝。645646        参数: pending 为有限原缓冲;validator 为跨批协议状态。返回: None。647        异常: 单批真实工作区超限或取消;不丢样本,也不切换计算后端。648        """649        count = sum(len(records) for records in pending.values())650        unicode_bytes = 0651        if self.owner.backend == 'numpy':652            for records in pending.values():653                # 各列依次打包,原始 Unicode 数组在返回数值列前释放,只取最大列峰值。654                widths = [len(item[2]) for item in records if type(item[2]) is str]655                unicode_bytes = max(unicode_bytes, len(widths) * max(widths, default=0) * 4)656        retained, workspace = self._pack_estimate(validator, count, len(pending), unicode_bytes)657        self._add('work', workspace)658        self.array_cost = retained659660    def after_pack(self, validator):661        """校验完成后把工作预留转为当前owned数组和保留协议状态。662663        参数: validator为已accept状态。返回: None。异常: 内部账本一致性错误。664        """665        state_cost = sum(512 + len(state[1]) * 256 for state in validator.pending.values())666        # 旧batch已被原生成器和包装游标交接;新数组预留早已申请,不重复申请。667        self._release('array')668        self._release('protocol')669        # 原对象由缓冲与新列共享,既有付款转交输出责任,不申请第二份费用。670        self._move('pending', 'array', self._pending_cost)671        self._move('work', 'array', self.array_cost)672        self._move('work', 'protocol', state_cost)673        self._release('work')674675    def clear_pending(self):676        """原生成器恢复并清空缓冲后重置计数,不重复抵销已转交的共享费用。677678        参数: 无。返回: None。异常: 账本一致性错误。679        """680        self._release('pending')681        self._sample_count = 0682        self._column_count.clear()683        self._string_layout.clear()684685    def end_file(self):686        """局部行/表头引用丢弃后抵销预算,跨文件样本与协议预算保留。687688        参数: 无。返回: None。异常: 账本一致性错误。689        """690        self.line_source = None691        for key in ('reading', 'header', 'row'):692            self._release(key)693694    def close(self):695        """释放本请求全部金额,不关闭他请求或删除输入文件。696697        参数: 无。返回: None,幂等。异常: 账本一致性错误。698        """699        if self.closed:700            return701        self._closing = True702        self.line_source = None703        for key in ('fixed', 'reading', 'header', 'row', 'pending', 'work', 'array', 'protocol'):704            self._release(key)705        if self._held_cost == 0:706            self.closed = True707            self.owner._resources.discard(self)708709710class _PhysicalLines:711    """每次readline前按剩余RAM申请上限;引用记录跨行继续累积reading金额。"""712713    def __init__(self, resources, handle):714        """保存真实只读句柄。参数: resources检查器/handle文件。返回: 新行游标。异常: 无。"""715        self.resources, self.handle = resources, handle716717    def __iter__(self):718        """返回物理行游标。参数: 无。返回: self。异常: 无。"""719        return self720721    def _retained_line_bytes(self, text, first_line):722        """计算当前逻辑记录仍需保存的解析字符和字段开销。723724        参数: text 为刚读取的物理行;first_line 表示该逻辑记录的第一行。725        返回: 字节估算。异常: 无预期异常。726        """727        if not text:728            return 0729        # 原文本、CSV 的 UTF32 字符缓冲及结果字符串按 16B/字符预留。730        # 每个分隔符另加字段对象/列表槽费用;引号内分隔符保守计入。731        # 固定容器按逻辑记录计一次,不能对引用单元格的每一行重复收 8KiB。732        return (256 if first_line else 0) + len(text) * 16 + text.count(self.resources.owner.spec.delimiter) * 48733734    def __next__(self):735        """有界读完整物理行,超限不把截断内容交给csv.reader。736737        参数: 无。返回: 含原换行的str;EOF停止。异常: BudgetExceededError/原I/O错误。738        """739        budget = self.resources.budget740        # 16B/字符先覆盖文本与字符缓冲;实际分隔符对应的字段费用在交给 CSV 解析器前另加。741        capacity = (budget.memory_budget_bytes - budget.ram_bytes - 8192) // 16 - 1742        if capacity <= 0:743            raise BudgetExceededError('CSV物理行/引用记录累积超出剩余RAM估算')744        amount = 8192 + (capacity + 1) * 16745        first_line = not self.resources._reading_cost746        self.resources._add('reading', amount)747        try:748            # 多读一字符仅判别完整物理行,不能把部分未闭合引号当另一记录。749            text = self.handle.readline(capacity + 1)750        except BaseException as exc:751            try:752                self.resources._release_amount('reading', amount)753            except BaseException as cleanup:754                exc.add_note(f'默认CSV物理行清理附加失败: {type(cleanup).__name__}')755            raise756        retained = self._retained_line_bytes(text, first_line)757        if retained > amount:758            self.resources._add('reading', retained - amount)759        else:760            self.resources._release_amount('reading', amount - retained)761        if len(text) > capacity:762            raise BudgetExceededError('CSV物理行超过剩余RAM允许长度,未解析截断行')763        if not text:764            raise StopIteration765        return text766767768class _CSVRecords:769    """包装原csv.reader,表头固定持有,当前/下一逻辑记录读取时预算重叠。"""770771    def __init__(self, resources, reader):772        """保存原解析器。参数: resources检查器/reader原csv.reader。返回: 新记录游标。异常: 无。"""773        self.resources, self.reader, self.first = resources, reader, True774775    def __iter__(self):776        """返回记录游标。参数: 无。返回: self。异常: 无。"""777        return self778779    def __next__(self):780        """推进原解析器,跨物理行累积到一条完整记录再转移预算。781782        参数: 无。返回: 原单元格list。异常: EOF/原csv错误/预算或I/O错误。783        """784        # 上一cells在调用next期间仍由原生成器保存,先完成新解析再释放旧行预算。785        cells = next(self.reader)786        key = 'header' if self.first else 'row'787        self.resources._release(key)788        self.resources._move('reading', key, self.resources._reading_cost)789        self.first = False790        return cells791
Chronaxis 用户手册 · 离线阅读