A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: move http upload state to heap (#657)

## Summary

The main motivation behind this PR was because `uploadBuffer` is
statically allocated, but only used when web server is enabled. This
results in 4KB of memory sitting idle most of the time.

As expected, 4KB of initial RAM is freed with this PR:

```
master:
RAM: [=== ] 32.5% (used 106508 bytes from 327680 bytes)

PR:
RAM: [=== ] 31.2% (used 102276 bytes from 327680 bytes)
```

## Additional Context

This also highlights the importance of only using statically-allocated
buffer when absolutely needed (for example, if the component is active
most of the time). Otherwise, it makes more sense to tie the buffer's
life cycle to its activity.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO

authored by

Xuan-Son Nguyen and committed by
GitHub
db659f3e 78d6e593

+81 -76
+59 -74
src/network/CrossPointWebServer.cpp
··· 104 104 server->on("/download", HTTP_GET, [this] { handleDownload(); }); 105 105 106 106 // Upload endpoint with special handling for multipart form data 107 - server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); }); 107 + server->on("/upload", HTTP_POST, [this] { handleUploadPost(upload); }, [this] { handleUpload(upload); }); 108 108 109 109 // Create folder endpoint 110 110 server->on("/mkdir", HTTP_POST, [this] { handleCreateFolder(); }); ··· 458 458 file.close(); 459 459 } 460 460 461 - // Static variables for upload handling 462 - static FsFile uploadFile; 463 - static String uploadFileName; 464 - static String uploadPath = "/"; 465 - static size_t uploadSize = 0; 466 - static bool uploadSuccess = false; 467 - static String uploadError = ""; 468 - 469 - // Upload write buffer - batches small writes into larger SD card operations 470 - // 4KB is a good balance: large enough to reduce syscall overhead, small enough 471 - // to keep individual write times short and avoid watchdog issues 472 - constexpr size_t UPLOAD_BUFFER_SIZE = 4096; // 4KB buffer 473 - static uint8_t uploadBuffer[UPLOAD_BUFFER_SIZE]; 474 - static size_t uploadBufferPos = 0; 475 - 476 461 // Diagnostic counters for upload performance analysis 477 462 static unsigned long uploadStartTime = 0; 478 463 static unsigned long totalWriteTime = 0; 479 464 static size_t writeCount = 0; 480 465 481 - static bool flushUploadBuffer() { 482 - if (uploadBufferPos > 0 && uploadFile) { 466 + static bool flushUploadBuffer(CrossPointWebServer::UploadState& state) { 467 + if (state.bufferPos > 0 && state.file) { 483 468 esp_task_wdt_reset(); // Reset watchdog before potentially slow SD write 484 469 const unsigned long writeStart = millis(); 485 - const size_t written = uploadFile.write(uploadBuffer, uploadBufferPos); 470 + const size_t written = state.file.write(state.buffer.data(), state.bufferPos); 486 471 totalWriteTime += millis() - writeStart; 487 472 writeCount++; 488 473 esp_task_wdt_reset(); // Reset watchdog after SD write 489 474 490 - if (written != uploadBufferPos) { 491 - Serial.printf("[%lu] [WEB] [UPLOAD] Buffer flush failed: expected %d, wrote %d\n", millis(), uploadBufferPos, 475 + if (written != state.bufferPos) { 476 + Serial.printf("[%lu] [WEB] [UPLOAD] Buffer flush failed: expected %d, wrote %d\n", millis(), state.bufferPos, 492 477 written); 493 - uploadBufferPos = 0; 478 + state.bufferPos = 0; 494 479 return false; 495 480 } 496 - uploadBufferPos = 0; 481 + state.bufferPos = 0; 497 482 } 498 483 return true; 499 484 } 500 485 501 - void CrossPointWebServer::handleUpload() const { 486 + void CrossPointWebServer::handleUpload(UploadState& state) const { 502 487 static size_t lastLoggedSize = 0; 503 488 504 489 // Reset watchdog at start of every upload callback - HTTP parsing can be slow ··· 516 501 // Reset watchdog - this is the critical 1% crash point 517 502 esp_task_wdt_reset(); 518 503 519 - uploadFileName = upload.filename; 520 - uploadSize = 0; 521 - uploadSuccess = false; 522 - uploadError = ""; 504 + state.fileName = upload.filename; 505 + state.size = 0; 506 + state.success = false; 507 + state.error = ""; 523 508 uploadStartTime = millis(); 524 509 lastLoggedSize = 0; 525 - uploadBufferPos = 0; 510 + state.bufferPos = 0; 526 511 totalWriteTime = 0; 527 512 writeCount = 0; 528 513 ··· 530 515 // Note: We use query parameter instead of form data because multipart form 531 516 // fields aren't available until after file upload completes 532 517 if (server->hasArg("path")) { 533 - uploadPath = server->arg("path"); 518 + state.path = server->arg("path"); 534 519 // Ensure path starts with / 535 - if (!uploadPath.startsWith("/")) { 536 - uploadPath = "/" + uploadPath; 520 + if (!state.path.startsWith("/")) { 521 + state.path = "/" + state.path; 537 522 } 538 523 // Remove trailing slash unless it's root 539 - if (uploadPath.length() > 1 && uploadPath.endsWith("/")) { 540 - uploadPath = uploadPath.substring(0, uploadPath.length() - 1); 524 + if (state.path.length() > 1 && state.path.endsWith("/")) { 525 + state.path = state.path.substring(0, state.path.length() - 1); 541 526 } 542 527 } else { 543 - uploadPath = "/"; 528 + state.path = "/"; 544 529 } 545 530 546 - Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str()); 531 + Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), state.fileName.c_str(), state.path.c_str()); 547 532 Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap()); 548 533 549 534 // Create file path 550 - String filePath = uploadPath; 535 + String filePath = state.path; 551 536 if (!filePath.endsWith("/")) filePath += "/"; 552 - filePath += uploadFileName; 537 + filePath += state.fileName; 553 538 554 539 // Check if file already exists - SD operations can be slow 555 540 esp_task_wdt_reset(); ··· 561 546 562 547 // Open file for writing - this can be slow due to FAT cluster allocation 563 548 esp_task_wdt_reset(); 564 - if (!SdMan.openFileForWrite("WEB", filePath, uploadFile)) { 565 - uploadError = "Failed to create file on SD card"; 549 + if (!SdMan.openFileForWrite("WEB", filePath, state.file)) { 550 + state.error = "Failed to create file on SD card"; 566 551 Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str()); 567 552 return; 568 553 } ··· 570 555 571 556 Serial.printf("[%lu] [WEB] [UPLOAD] File created successfully: %s\n", millis(), filePath.c_str()); 572 557 } else if (upload.status == UPLOAD_FILE_WRITE) { 573 - if (uploadFile && uploadError.isEmpty()) { 558 + if (state.file && state.error.isEmpty()) { 574 559 // Buffer incoming data and flush when buffer is full 575 560 // This reduces SD card write operations and improves throughput 576 561 const uint8_t* data = upload.buf; 577 562 size_t remaining = upload.currentSize; 578 563 579 564 while (remaining > 0) { 580 - const size_t space = UPLOAD_BUFFER_SIZE - uploadBufferPos; 565 + const size_t space = UploadState::UPLOAD_BUFFER_SIZE - state.bufferPos; 581 566 const size_t toCopy = (remaining < space) ? remaining : space; 582 567 583 - memcpy(uploadBuffer + uploadBufferPos, data, toCopy); 584 - uploadBufferPos += toCopy; 568 + memcpy(state.buffer.data() + state.bufferPos, data, toCopy); 569 + state.bufferPos += toCopy; 585 570 data += toCopy; 586 571 remaining -= toCopy; 587 572 588 573 // Flush buffer when full 589 - if (uploadBufferPos >= UPLOAD_BUFFER_SIZE) { 590 - if (!flushUploadBuffer()) { 591 - uploadError = "Failed to write to SD card - disk may be full"; 592 - uploadFile.close(); 574 + if (state.bufferPos >= UploadState::UPLOAD_BUFFER_SIZE) { 575 + if (!flushUploadBuffer(state)) { 576 + state.error = "Failed to write to SD card - disk may be full"; 577 + state.file.close(); 593 578 return; 594 579 } 595 580 } 596 581 } 597 582 598 - uploadSize += upload.currentSize; 583 + state.size += upload.currentSize; 599 584 600 585 // Log progress every 100KB 601 - if (uploadSize - lastLoggedSize >= 102400) { 586 + if (state.size - lastLoggedSize >= 102400) { 602 587 const unsigned long elapsed = millis() - uploadStartTime; 603 - const float kbps = (elapsed > 0) ? (uploadSize / 1024.0) / (elapsed / 1000.0) : 0; 604 - Serial.printf("[%lu] [WEB] [UPLOAD] %d bytes (%.1f KB), %.1f KB/s, %d writes\n", millis(), uploadSize, 605 - uploadSize / 1024.0, kbps, writeCount); 606 - lastLoggedSize = uploadSize; 588 + const float kbps = (elapsed > 0) ? (state.size / 1024.0) / (elapsed / 1000.0) : 0; 589 + Serial.printf("[%lu] [WEB] [UPLOAD] %d bytes (%.1f KB), %.1f KB/s, %d writes\n", millis(), state.size, 590 + state.size / 1024.0, kbps, writeCount); 591 + lastLoggedSize = state.size; 607 592 } 608 593 } 609 594 } else if (upload.status == UPLOAD_FILE_END) { 610 - if (uploadFile) { 595 + if (state.file) { 611 596 // Flush any remaining buffered data 612 - if (!flushUploadBuffer()) { 613 - uploadError = "Failed to write final data to SD card"; 597 + if (!flushUploadBuffer(state)) { 598 + state.error = "Failed to write final data to SD card"; 614 599 } 615 - uploadFile.close(); 600 + state.file.close(); 616 601 617 - if (uploadError.isEmpty()) { 618 - uploadSuccess = true; 602 + if (state.error.isEmpty()) { 603 + state.success = true; 619 604 const unsigned long elapsed = millis() - uploadStartTime; 620 - const float avgKbps = (elapsed > 0) ? (uploadSize / 1024.0) / (elapsed / 1000.0) : 0; 605 + const float avgKbps = (elapsed > 0) ? (state.size / 1024.0) / (elapsed / 1000.0) : 0; 621 606 const float writePercent = (elapsed > 0) ? (totalWriteTime * 100.0 / elapsed) : 0; 622 607 Serial.printf("[%lu] [WEB] [UPLOAD] Complete: %s (%d bytes in %lu ms, avg %.1f KB/s)\n", millis(), 623 - uploadFileName.c_str(), uploadSize, elapsed, avgKbps); 608 + state.fileName.c_str(), state.size, elapsed, avgKbps); 624 609 Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(), 625 610 writeCount, totalWriteTime, writePercent); 626 611 627 612 // Clear epub cache to prevent stale metadata issues when overwriting files 628 - String filePath = uploadPath; 613 + String filePath = state.path; 629 614 if (!filePath.endsWith("/")) filePath += "/"; 630 - filePath += uploadFileName; 615 + filePath += state.fileName; 631 616 clearEpubCacheIfNeeded(filePath); 632 617 } 633 618 } 634 619 } else if (upload.status == UPLOAD_FILE_ABORTED) { 635 - uploadBufferPos = 0; // Discard buffered data 636 - if (uploadFile) { 637 - uploadFile.close(); 620 + state.bufferPos = 0; // Discard buffered data 621 + if (state.file) { 622 + state.file.close(); 638 623 // Try to delete the incomplete file 639 - String filePath = uploadPath; 624 + String filePath = state.path; 640 625 if (!filePath.endsWith("/")) filePath += "/"; 641 - filePath += uploadFileName; 626 + filePath += state.fileName; 642 627 SdMan.remove(filePath.c_str()); 643 628 } 644 - uploadError = "Upload aborted"; 629 + state.error = "Upload aborted"; 645 630 Serial.printf("[%lu] [WEB] Upload aborted\n", millis()); 646 631 } 647 632 } 648 633 649 - void CrossPointWebServer::handleUploadPost() const { 650 - if (uploadSuccess) { 651 - server->send(200, "text/plain", "File uploaded successfully: " + uploadFileName); 634 + void CrossPointWebServer::handleUploadPost(UploadState& state) const { 635 + if (state.success) { 636 + server->send(200, "text/plain", "File uploaded successfully: " + state.fileName); 652 637 } else { 653 - const String error = uploadError.isEmpty() ? "Unknown error during upload" : uploadError; 638 + const String error = state.error.isEmpty() ? "Unknown error during upload" : state.error; 654 639 server->send(400, "text/plain", error); 655 640 } 656 641 }
+22 -2
src/network/CrossPointWebServer.h
··· 1 1 #pragma once 2 2 3 + #include <SDCardManager.h> 3 4 #include <WebServer.h> 4 5 #include <WebSocketsServer.h> 5 6 #include <WiFiUdp.h> ··· 28 29 unsigned long lastCompleteAt = 0; 29 30 }; 30 31 32 + // Used by POST upload handler 33 + struct UploadState { 34 + FsFile file; 35 + String fileName; 36 + String path = "/"; 37 + size_t size = 0; 38 + bool success = false; 39 + String error = ""; 40 + 41 + // Upload write buffer - batches small writes into larger SD card operations 42 + // 4KB is a good balance: large enough to reduce syscall overhead, small enough 43 + // to keep individual write times short and avoid watchdog issues 44 + static constexpr size_t UPLOAD_BUFFER_SIZE = 4096; // 4KB buffer 45 + std::vector<uint8_t> buffer; 46 + size_t bufferPos = 0; 47 + 48 + UploadState() { buffer.resize(UPLOAD_BUFFER_SIZE); } 49 + } upload; 50 + 31 51 CrossPointWebServer(); 32 52 ~CrossPointWebServer(); 33 53 ··· 74 94 void handleFileList() const; 75 95 void handleFileListData() const; 76 96 void handleDownload() const; 77 - void handleUpload() const; 78 - void handleUploadPost() const; 97 + void handleUpload(UploadState& state) const; 98 + void handleUploadPost(UploadState& state) const; 79 99 void handleCreateFolder() const; 80 100 void handleDelete() const; 81 101 };