diff --git a/src/main/java/com/kamco/cd/training/common/service/GpuDmonReader.java b/src/main/java/com/kamco/cd/training/common/service/GpuDmonReader.java index f6e11b5..024ec79 100644 --- a/src/main/java/com/kamco/cd/training/common/service/GpuDmonReader.java +++ b/src/main/java/com/kamco/cd/training/common/service/GpuDmonReader.java @@ -3,6 +3,8 @@ package com.kamco.cd.training.common.service; import jakarta.annotation.PostConstruct; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.util.ArrayDeque; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import lombok.extern.log4j.Log4j2; @@ -13,19 +15,33 @@ import org.springframework.stereotype.Component; public class GpuDmonReader { // ========================= - // GPU 사용률 저장소 + // GPU 사용률 히스토리 저장소 // key: GPU index (0,1,2...) - // value: 현재 GPU 사용률 (%) - // ConcurrentHashMap → 멀티스레드 안전 + // value: 최근 WINDOW_SIZE 개의 sm 사용률 샘플 (1초 간격) + // ConcurrentHashMap → 멀티스레드 안전 (deque 접근은 synchronized) // ========================= - private final Map gpuUtilMap = new ConcurrentHashMap<>(); + private static final int WINDOW_SIZE = 10; // 10초 평균 + + private final Map> historyMap = new ConcurrentHashMap<>(); // ========================= // 외부 조회용 + // GPU 개수만큼 10초 평균값 반환 // SystemMonitorService에서 호출 // ========================= public Map getGpuUtilMap() { - return gpuUtilMap; + Map result = new HashMap<>(); + historyMap.forEach( + (gpu, deque) -> { + synchronized (deque) { + int avg = + deque.isEmpty() + ? 0 + : (int) Math.round(deque.stream().mapToInt(Integer::intValue).average().orElse(0)); + result.put(gpu, avg); + } + }); + return result; } // ========================= @@ -76,10 +92,14 @@ public class GpuDmonReader { // -s u → GPU utilization만 출력 ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u"); + // stderr를 stdout에 합쳐서 소비 — 소비하지 않으면 stderr 버퍼가 가득 차 + // nvidia-smi 프로세스 자체가 block되어 stdout도 멈춤 + pb.redirectErrorStream(true); + Process process = pb.start(); // 프로세스 실행 후 stdout 읽기 try (BufferedReader br = - new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) { + new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; @@ -96,21 +116,24 @@ public class GpuDmonReader { String[] parts = line.split("\\s+"); // 첫 번째 값이 GPU index인지 확인 - if (!parts[0].matches("\\d+")) continue; + if (parts.length < 2 || !parts[0].matches("\\d+")) continue; int index = Integer.parseInt(parts[0]); try { - // 두 번째 값이 GPU 사용률 (sm) + // 두 번째 값이 GPU 사용률 (sm), "-"이면 N/A (GPU 미활성) int util = Integer.parseInt(parts[1]); + pushSample(index, util); + log.debug("[GpuDmon] gpu={} sm={}%", index, util); - // 최신 값 갱신 - gpuUtilMap.put(index, util); - - } catch (Exception ignored) { - // 파싱 실패 시 무시 + } catch (NumberFormatException e) { + // "-" 등 숫자가 아닌 값 → GPU 비활성 상태이므로 0으로 기록 + pushSample(index, 0); + log.debug("[GpuDmon] gpu={} sm=N/A (raw={}), stored as 0", index, parts[1]); } } + } finally { + process.destroy(); } // 여기까지 왔다는 건 dmon 프로세스 종료됨 @@ -118,6 +141,20 @@ public class GpuDmonReader { throw new IllegalStateException("dmon stopped"); } + // ========================= + // 샘플 추가 — 윈도우 초과 시 가장 오래된 값 제거 + // ========================= + private void pushSample(int gpuIndex, int util) { + ArrayDeque deque = + historyMap.computeIfAbsent(gpuIndex, k -> new ArrayDeque<>(WINDOW_SIZE)); + synchronized (deque) { + deque.addLast(util); + if (deque.size() > WINDOW_SIZE) { + deque.pollFirst(); + } + } + } + // ========================= // nvidia-smi 존재 여부 확인 // ========================= diff --git a/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java b/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java index 0231640..a1177f1 100644 --- a/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java +++ b/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java @@ -1,7 +1,9 @@ package com.kamco.cd.training.filemanager.dto; +import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; @@ -274,5 +276,13 @@ public class FileManagerDto { @Schema(description = "디스크 사용률 (%)", example = "50.5") private Double usagePercentage; + + + @JsonFormatDttm + private ZonedDateTime lastModifiedDate; + + public ZonedDateTime getLastModifiedDate() { + return ZonedDateTime.now(); + } } } diff --git a/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java b/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java index 4e92f67..df018a9 100644 --- a/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java +++ b/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java @@ -617,43 +617,37 @@ public class FileManagerService { .build(); } - // 디렉토리 사용량 계산 (재귀적으로 모든 파일 크기 합산) - final long[] usedSize = {0}; - final int[] fileCount = {0}; - final int[] directoryCount = {0}; + // 디렉토리 사용량 계산 — du -sb 사용 (walkFileTree 대비 수배 빠름) + // fileCount/directoryCount는 du가 제공하지 않으므로 -1(미집계) 반환 + long usedSize = 0; - log.info("[StorageSpace] walkFileTree 시작 - path={}", directoryPath); + log.info("[StorageSpace] du 시작 - path={}", directoryPath); try { - Files.walkFileTree( - directory, - new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - usedSize[0] += attrs.size(); - fileCount[0]++; - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - if (!dir.equals(directory)) { - directoryCount[0]++; - } - return FileVisitResult.CONTINUE; - } - }); + ProcessBuilder duPb = new ProcessBuilder("du", "-sb", directoryPath); + duPb.redirectErrorStream(true); + Process duProcess = duPb.start(); + try (java.io.BufferedReader br = + new java.io.BufferedReader( + new java.io.InputStreamReader(duProcess.getInputStream()))) { + String line = br.readLine(); + if (line != null) { + // du -sb 출력 형식: "12345678\t/path/to/dir" + String[] parts = line.split("\t", 2); + usedSize = Long.parseLong(parts[0].trim()); + } + } + int exitCode = duProcess.waitFor(); log.info( - "[StorageSpace] walkFileTree 완료 - fileCount={}, dirCount={}, usedSize={} bytes ({})", - fileCount[0], - directoryCount[0], - usedSize[0], - formatFileSize(usedSize[0])); - } catch (IOException e) { - log.error("디렉토리 용량 계산 중 오류 발생: {}", directoryPath, e); + "[StorageSpace] du 완료 - exitCode={}, usedSize={} bytes ({})", + exitCode, + usedSize, + formatFileSize(usedSize)); + } catch (Exception e) { + log.error("[StorageSpace] du 실행 실패: {}", directoryPath, e); return FileManagerDto.StorageSpaceRes.builder() .directoryPath(directoryPath) - .fileCount(0) - .directoryCount(0) + .fileCount(-1) + .directoryCount(-1) .usedSize(0L) .usedSizeFormatted("0 B") .totalDiskSpace(0L) @@ -701,10 +695,10 @@ public class FileManagerService { return FileManagerDto.StorageSpaceRes.builder() .directoryPath(directoryPath) - .fileCount(fileCount[0]) - .directoryCount(directoryCount[0]) - .usedSize(usedSize[0]) - .usedSizeFormatted(formatFileSize(usedSize[0])) + .fileCount(-1) + .directoryCount(-1) + .usedSize(usedSize) + .usedSizeFormatted(formatFileSize(usedSize)) .totalDiskSpace(totalDiskSpace) .totalDiskSpaceFormatted(formatFileSize(totalDiskSpace)) .freeSpace(freeSpace)