/* Append-only, little-endian binary64 blocks. No output evaluation during solve. * Each block: SIMBLK01, u64 sequence/rows/columns/crc32, column-major payload, * COMMIT01. A truncated/uncommitted tail is never a valid block. * Synchronous buffered writes provide backpressure without a producer queue. */ #include "runtime.h" #include "json_numbers.h" #include #include #include #include #include #ifndef NATIVE_SAMPLE_BUFFER_BYTES #define NATIVE_SAMPLE_BUFFER_BYTES 1048576u #endif _Static_assert(sizeof(double)==8 && DBL_MANT_DIG==53 && DBL_MAX_EXP==1024, "Result blocks require IEEE-754 binary64"); int64_t native_file_tell(FILE *f) { /* MinGW/MSVCRT can overcount a buffered final newline in _ftelli64 even for a binary FILE. Query the committed byte position instead. */ if(fflush(f)) return -1; #ifdef _WIN32 return _ftelli64(f); #else return (int64_t)ftello(f); #endif } static int seek_file(FILE *f, uint64_t offset) { if(offset>INT64_MAX) return 0; #ifdef _WIN32 return _fseeki64(f,(int64_t)offset,SEEK_SET)==0; #else return fseeko(f,(off_t)offset,SEEK_SET)==0; #endif } static void put64(unsigned char *p,uint64_t n) { for(int i=0;i<8;i++) {p[i]=(unsigned char)n;n>>=8;} } static uint64_t get64(const unsigned char *p) { uint64_t n=0;for(int i=7;i>=0;i--) n=(n<<8)|p[i];return n; } static void little_endian(double *values,size_t count) { const uint16_t one=1; if(*(const unsigned char *)&one) return; unsigned char *p=(unsigned char *)values; for(size_t i=0;i>1)^((c&1)?UINT32_C(0xedb88320):0); table[i]=c; } uint32_t c=UINT32_MAX;const unsigned char *p=data; for(size_t i=0;i>8); return c^UINT32_MAX; } static int storage_error(NativeRun *r,const char *reason,const char *message) { r->storage_failed=1; /* Storage failure must also be visible when flushing a cancelled run. */ r->status=2;r->failure_reason=reason;r->failure_operation="sample-storage"; snprintf(r->message_buffer,sizeof(r->message_buffer),"%s",message); r->message=r->message_buffer; r->count=r->committed;r->buffered=0; return 0; } static int write_block(FILE *f,uint64_t sequence,size_t rows,size_t columns,double *data) { size_t length=rows*columns*sizeof(double); unsigned char header[40];memcpy(header,"SIMBLK01",8); put64(header+8,sequence);put64(header+16,rows);put64(header+24,columns); little_endian(data,rows*columns); put64(header+32,checksum(data,length)); int ok=fwrite(header,1,sizeof(header),f)==sizeof(header) && fwrite(data,1,length,f)==length && fwrite("COMMIT01",1,8,f)==8 && fflush(f)==0 && !ferror(f); little_endian(data,rows*columns); return ok; } static int read_block(FILE *f,uint64_t sequence,size_t rows,size_t columns,double *data) { unsigned char header[40],tail[8];size_t length=rows*columns*sizeof(double); if(fread(header,1,40,f)!=40 || memcmp(header,"SIMBLK01",8) || get64(header+8)!=sequence || get64(header+16)!=rows || get64(header+24)!=columns || fread(data,1,length,f)!=length || fread(tail,1,8,f)!=8 || memcmp(tail,"COMMIT01",8) || get64(header+32)!=checksum(data,length)) return 0; little_endian(data,rows*columns);return 1; } int native_samples_flush(NativeRun *r) { if(r->storage_failed) return 0; if(!r->buffered) return 1; size_t rows=r->buffered; /* Reserve both this state block and its future output block before writing. The parent serializes quota grants across concurrent simulations. */ uint64_t bytes=96+(uint64_t)rows*(NSTATES+NOUTPUTS+2)*8; if(r->storage_control) { char reply[16]; fprintf(stderr,"{\"phase\":\"storage-reserve\",\"bytes\":%llu}\n",(unsigned long long)bytes); fflush(stderr); if(!fgets(reply,sizeof(reply),stdin)) return storage_error(r,"storage-control","Result storage controller disconnected; complete blocks were retained."); if(!strcmp(reply,"full\n")) return storage_error(r,"storage-quota","Result storage quota exhausted; complete blocks were retained."); if(strcmp(reply,"ok\n")) return storage_error(r,"storage-io","Result storage I/O failed; complete blocks were retained."); } double *data=malloc(rows*(NSTATES+1)*sizeof(double)); if(!data) return storage_error(r,"storage-memory","Cannot allocate result block."); memcpy(data,r->times,rows*sizeof(double)); for(int j=0;jstates[i*NSTATES+j]; int ok=write_block(r->sample_file,r->committed/r->capacity,rows,NSTATES+1,data); free(data); if(!ok) return storage_error(r,"storage-io","Cannot write result block; complete blocks were retained."); r->committed+=rows;r->buffered=0;return 1; } int native_samples_append(NativeRun *r,double t,const double *state) { if(r->storage_failed) return 0; if(!r->capacity) { size_t columns=NSTATES>NOUTPUTS?NSTATES+1:NOUTPUTS+1; r->capacity=NATIVE_SAMPLE_BUFFER_BYTES/(columns*sizeof(double)); if(!r->capacity) r->capacity=1; if(r->capacity>1024) r->capacity=1024; r->times=malloc(r->capacity*sizeof(double)); r->states=malloc(r->capacity*NSTATES*sizeof(double)); r->sample_file=r->sample_path?fopen(r->sample_path,"w+b"):tmpfile(); if(!r->times || !r->states || !r->sample_file) return storage_error(r,"storage-io","Cannot initialize result storage."); setvbuf(r->sample_file,NULL,_IOFBF,65536); } /* Hold the newest sample until a distinct time arrives, preserving the pre-existing same-time replacement policy even at block boundaries. */ if(r->buffered && r->times[r->buffered-1]==t) {r->buffered--;r->count--;} if(r->buffered==r->capacity && !native_samples_flush(r)) return 0; r->times[r->buffered]=t; memcpy(r->states+r->buffered*NSTATES,state,NSTATES*sizeof(double)); r->buffered++;r->count++;return 1; } int native_samples_outputs(NativeRun *r) { if(!r->committed) return 1; r->output_file=r->output_blocks_path?fopen(r->output_blocks_path,"w+b"):tmpfile(); if(!r->output_file || !seek_file(r->sample_file,0)) return 0; setvbuf(r->output_file,NULL,_IOFBF,65536); double *states=malloc(r->capacity*(NSTATES+1)*sizeof(double)); double *outputs=malloc(r->capacity*(NOUTPUTS+1)*sizeof(double)); int ok=states && outputs; for(size_t start=0;ok && startcommitted;start+=r->capacity) { size_t rows=r->committed-start;if(rows>r->capacity) rows=r->capacity; ok=read_block(r->sample_file,start/r->capacity,rows,NSTATES+1,states); for(size_t i=0;ok && ioutput_file,start/r->capacity,rows,NOUTPUTS+1,outputs); if(ok) r->output_count+=rows; } free(states);free(outputs);return ok; } int native_samples_json(NativeRun *r,FILE *f,size_t column) { if(column>NOUTPUTS || r->output_count!=r->count || fputc('[',f)==EOF) return 0; double *values=malloc((r->capacity?r->capacity:1)*sizeof(double)); if(!values) return 0; int ok=1; for(size_t start=0;ok && startcount;start+=r->capacity) { size_t rows=r->count-start;if(rows>r->capacity) rows=r->capacity; uint64_t offset=(uint64_t)(start/r->capacity)*(48+(uint64_t)r->capacity*(NOUTPUTS+1)*8); ok=seek_file(r->output_file,offset+40+(uint64_t)column*rows*8) && fread(values,sizeof(double),rows,r->output_file)==rows; if(ok) {little_endian(values,rows);ok=native_json_write_values(f,values,rows,1,start!=0);} } free(values);return ok && fputc(']',f)!=EOF; }