33"""
44
55import base64
6+ import copy
67import json
78import logging
89import os
910import random
1011import shutil
1112import time
1213import uuid
13- from dataclasses import asdict , dataclass , field , fields
14+ from dataclasses import asdict , dataclass , field , fields , is_dataclass
1415
1516# FileLock removed - no longer needed with threaded parallel processing
1617from typing import Any , Dict , List , Optional , Set , Tuple , Union
@@ -40,6 +41,29 @@ def _safe_avg_metrics(metrics: Dict[str, Any]) -> float:
4041 return sum (numeric_values ) / max (1 , len (numeric_values )) if numeric_values else 0.0
4142
4243
44+ def _copy_field_value (value : Any ) -> Any :
45+ """
46+ Produce the same value dataclasses.asdict() would produce for one field.
47+
48+ asdict() walks every node through _asdict_inner() and copy.deepcopy(), which is
49+ a large amount of dispatch for the plain str/int/float/dict/list values a Program
50+ actually holds. The fast paths below cover those; anything else falls back to the
51+ same helpers asdict() itself would use, so the result is unchanged.
52+ """
53+ t = type (value )
54+ if t is str or t is int or t is float or t is bool or value is None :
55+ return value
56+ if t is dict :
57+ return {k : _copy_field_value (v ) for k , v in value .items ()}
58+ if t is list :
59+ return [_copy_field_value (v ) for v in value ]
60+ if t is tuple :
61+ return tuple (_copy_field_value (v ) for v in value )
62+ if is_dataclass (value ) and not isinstance (value , type ):
63+ return asdict (value )
64+ return copy .deepcopy (value )
65+
66+
4367@dataclass
4468class Program :
4569 """Represents a program in the database"""
@@ -80,7 +104,8 @@ class Program:
80104
81105 def to_dict (self ) -> Dict [str , Any ]:
82106 """Convert to dictionary representation"""
83- return asdict (self )
107+ values = self .__dict__
108+ return {name : _copy_field_value (values [name ]) for name in _PROGRAM_FIELD_NAMES }
84109
85110 @classmethod
86111 def from_dict (cls , data : Dict [str , Any ]) -> "Program" :
@@ -112,6 +137,11 @@ def from_dict(cls, data: Dict[str, Any]) -> "Program":
112137 return cls (** filtered_data )
113138
114139
140+ # Field order of Program, resolved once. to_dict() walks this instead of calling
141+ # dataclasses.fields() on every call.
142+ _PROGRAM_FIELD_NAMES = tuple (f .name for f in fields (Program ))
143+
144+
115145class ProgramDatabase :
116146 """
117147 Database for storing and sampling programs during evolution
@@ -121,9 +151,18 @@ class ProgramDatabase:
121151 It also tracks the absolute best program separately to ensure it's never lost.
122152 """
123153
154+ # Bound on _code_shape_cache. A class attribute rather than an instance one so it
155+ # is available to _fast_code_diversity from inside __init__ itself.
156+ _CODE_SHAPE_CACHE_SIZE = 2048
157+
124158 def __init__ (self , config : DatabaseConfig ):
125159 self .config = config
126160
161+ # Bound before anything else in __init__: load() below reaches
162+ # log_island_status() -> get_island_stats() -> _calculate_island_diversity()
163+ # -> _fast_code_diversity(), which needs this cache.
164+ self ._code_shape_cache : Dict [str , Tuple [int , int , frozenset ]] = {}
165+
127166 # In-memory program storage
128167 self .programs : Dict [str , Program ] = {}
129168
@@ -2092,6 +2131,24 @@ def _calculate_island_diversity(self, programs: List[Program]) -> float:
20922131
20932132 return total_diversity / max (1 , comparisons )
20942133
2134+ def _code_shape (self , code : str ) -> Tuple [int , int , frozenset ]:
2135+ """
2136+ Length, newline count and character set of one code string, memoized.
2137+
2138+ _get_cached_diversity() compares one program against the whole reference set,
2139+ so the same reference strings are re-scanned once per comparison, and the
2140+ program's own string is re-scanned once per reference entry. Each scan is
2141+ O(len(code)); the derived values only depend on the string itself.
2142+ """
2143+ shape = self ._code_shape_cache .get (code )
2144+ if shape is None :
2145+ shape = (len (code ), code .count ("\n " ), frozenset (code ))
2146+ if len (self ._code_shape_cache ) >= self ._CODE_SHAPE_CACHE_SIZE :
2147+ # Same insertion-order eviction the diversity cache uses
2148+ del self ._code_shape_cache [next (iter (self ._code_shape_cache ))]
2149+ self ._code_shape_cache [code ] = shape
2150+ return shape
2151+
20952152 def _fast_code_diversity (self , code1 : str , code2 : str ) -> float :
20962153 """
20972154 Fast approximation of code diversity using simple metrics
@@ -2101,18 +2158,16 @@ def _fast_code_diversity(self, code1: str, code2: str) -> float:
21012158 if code1 == code2 :
21022159 return 0.0
21032160
2161+ len1 , lines1 , chars1 = self ._code_shape (code1 )
2162+ len2 , lines2 , chars2 = self ._code_shape (code2 )
2163+
21042164 # Length difference (scaled to reasonable range)
2105- len1 , len2 = len (code1 ), len (code2 )
21062165 length_diff = abs (len1 - len2 )
21072166
21082167 # Line count difference
2109- lines1 = code1 .count ("\n " )
2110- lines2 = code2 .count ("\n " )
21112168 line_diff = abs (lines1 - lines2 )
21122169
21132170 # Simple character set difference
2114- chars1 = set (code1 )
2115- chars2 = set (code2 )
21162171 char_diff = len (chars1 .symmetric_difference (chars2 ))
21172172
21182173 # Combine metrics (scaled to match original edit distance range)
@@ -2206,9 +2261,10 @@ def _cache_diversity_value(self, code_hash: int, diversity: float) -> None:
22062261 """Cache a diversity value with LRU eviction"""
22072262 # Check if cache is full
22082263 if len (self .diversity_cache ) >= self .diversity_cache_size :
2209- # Remove oldest entry
2210- oldest_hash = min (self .diversity_cache .items (), key = lambda x : x [1 ]["timestamp" ])[0 ]
2211- del self .diversity_cache [oldest_hash ]
2264+ # Remove oldest entry. Entries are inserted in increasing timestamp order
2265+ # and dicts preserve insertion order, so the first key is the same entry
2266+ # the previous min()-over-timestamps scan selected, without the O(n) scan.
2267+ del self .diversity_cache [next (iter (self .diversity_cache ))]
22122268
22132269 # Add new entry
22142270 self .diversity_cache [code_hash ] = {"value" : diversity , "timestamp" : time .time ()}
@@ -2239,9 +2295,13 @@ def _update_feature_stats(self, feature_name: str, value: float) -> None:
22392295 stats ["max" ] = max (stats ["max" ], value )
22402296
22412297 # Keep recent values for more sophisticated scaling methods
2242- stats ["values" ].append (value )
2243- if len (stats ["values" ]) > 1000 : # Limit memory usage
2244- stats ["values" ] = stats ["values" ][- 1000 :]
2298+ values = stats ["values" ]
2299+ values .append (value )
2300+ if len (values ) > 1000 : # Limit memory usage
2301+ # The list is trimmed one element at a time, so dropping the head in place
2302+ # is the same window the [-1000:] slice produced, without allocating and
2303+ # copying a fresh 1000-element list on every call once the window is full.
2304+ del values [0 ]
22452305
22462306 def _scale_feature_value (self , feature_name : str , value : float ) -> float :
22472307 """
0 commit comments