Files
SystemSimulationApp/native/runtime/sample_storage.c
T

178 lines
8.2 KiB
C

/* 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 <stdlib.h>
#include <string.h>
#include <float.h>
#include <limits.h>
#include <sys/types.h>
#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<count;i++,p+=8) for(int j=0;j<4;j++) {
unsigned char v=p[j];p[j]=p[7-j];p[7-j]=v;
}
}
static uint32_t checksum(const void *data,size_t length) {
uint32_t table[256];
for(unsigned i=0;i<256;i++) {
uint32_t c=i;
for(int j=0;j<8;j++) c=(c>>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<length;i++) c=table[(c^p[i])&255]^(c>>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;j<NSTATES;j++) for(size_t i=0;i<rows;i++)
data[(j+1)*rows+i]=r->states[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 && start<r->committed;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 && i<rows;i++) {
double y[NSTATES],dy[NSTATES],w[NOUTPUTS];
for(int j=0;j<NSTATES;j++) y[j]=states[(j+1)*rows+i];
ok=model_eval(states[i],y,dy,w);
outputs[i]=states[i];
if(ok) for(int j=0;j<NOUTPUTS;j++) outputs[(j+1)*rows+i]=w[j];
}
if(ok) ok=write_block(r->output_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 && start<r->count;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;
}