blob: ee8bf55f5880a34506a156c02c2f5501a8674b04 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iotdb;
import org.apache.tsfile.file.metadata.enums.CompressionType;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
/**
* Inspect a single WAL file and print size breakdowns for its major sections.
*
* <p>Example:
*
* <pre>
* java ... org.apache.iotdb.ConsensusSubscriptionWalFileAnalyzer D:\path\to\_12-25000-1.wal
* </pre>
*/
public class ConsensusSubscriptionWalFileAnalyzer {
private static final String V1_MAGIC = "WAL";
private static final String V2_MAGIC = "V2-WAL";
private static final String V3_MAGIC = "V3-WAL";
// Segment header layout:
// [compression type: 1 byte][payload size on disk: 4 bytes], plus an extra 4-byte
// uncompressed-size field for compressed segments.
private static final int SEGMENT_HEADER_BASE_BYTES = Byte.BYTES + Integer.BYTES;
private static final int COMPRESSED_SEGMENT_EXTRA_HEADER_BYTES = Integer.BYTES;
// Footer layout suffix:
// [WAL file info end marker: 1 byte][metadata size: 4 bytes][tail magic string].
private static final int WAL_FILE_INFO_END_MARKER_BYTES = Byte.BYTES;
private static final int METADATA_SIZE_FIELD_BYTES = Integer.BYTES;
private static final int V3_EMPTY_METADATA_REMAINING_WITHOUT_MEMTABLE_COUNT =
Long.BYTES * 2 + Short.BYTES * 2 + Integer.BYTES;
public static void main(final String[] args) throws Exception {
if (args.length == 0 || "--help".equals(args[0]) || "-h".equals(args[0])) {
printUsage();
return;
}
final File walFile = new File(args[0]);
if (!walFile.isFile()) {
throw new IllegalArgumentException("WAL file does not exist: " + walFile.getAbsolutePath());
}
final WalFileAnalysis analysis = analyze(walFile);
printAnalysis(analysis);
}
private static void printUsage() {
System.out.println("Usage:");
System.out.println(
" java ... org.apache.iotdb.ConsensusSubscriptionWalFileAnalyzer <wal-file-path>");
}
private static WalFileAnalysis analyze(final File walFile) throws IOException {
try (FileChannel channel = FileChannel.open(walFile.toPath(), StandardOpenOption.READ)) {
final long totalBytes = channel.size();
final String version = detectVersion(channel, totalBytes);
final int headMagicBytes = getHeadMagicBytes(version);
final int tailMagicBytes = getTailMagicBytes(version);
final WalFileAnalysis analysis = new WalFileAnalysis(walFile, version, totalBytes);
analysis.headMagicBytes = Math.min(totalBytes, headMagicBytes);
if (totalBytes <= headMagicBytes) {
analysis.note = "header-only WAL file (magic only, no body/footer)";
return analysis;
}
if (!hasTrailingMagic(channel, totalBytes, version)) {
analysis.note = "missing trailing magic/footer, file may be open or broken";
return analysis;
}
analysis.tailMagicBytes = tailMagicBytes;
analysis.metadataSizeFieldBytes = METADATA_SIZE_FIELD_BYTES;
final long metadataSizeFieldPos = totalBytes - tailMagicBytes - METADATA_SIZE_FIELD_BYTES;
if (metadataSizeFieldPos < headMagicBytes) {
analysis.note = "invalid metadata size position";
return analysis;
}
final int metadataBytes = readInt(channel, metadataSizeFieldPos);
analysis.metadataBytes = metadataBytes;
analysis.footerStartOffset = metadataSizeFieldPos - metadataBytes;
if (analysis.footerStartOffset < headMagicBytes) {
analysis.note = "invalid footer start offset";
return analysis;
}
final long markerOffset = analysis.footerStartOffset - WAL_FILE_INFO_END_MARKER_BYTES;
if (markerOffset < headMagicBytes) {
analysis.note = "invalid end-marker offset";
return analysis;
}
analysis.endMarkerBytes = WAL_FILE_INFO_END_MARKER_BYTES;
analysis.segmentStartOffset = headMagicBytes;
analysis.segmentEndOffsetExclusive = markerOffset;
analysis.segmentRegionBytes = Math.max(0L, markerOffset - headMagicBytes);
scanSegments(channel, analysis);
parseFooter(channel, analysis);
return analysis;
}
}
private static void scanSegments(final FileChannel channel, final WalFileAnalysis analysis)
throws IOException {
long offset = analysis.segmentStartOffset;
while (offset < analysis.segmentEndOffsetExclusive) {
if (analysis.segmentEndOffsetExclusive - offset < SEGMENT_HEADER_BASE_BYTES) {
analysis.segmentParseWarning =
"remaining bytes are smaller than a segment header at offset " + offset;
return;
}
final ByteBuffer headerBuffer = ByteBuffer.allocate(SEGMENT_HEADER_BASE_BYTES);
readFully(channel, headerBuffer, offset);
headerBuffer.flip();
final CompressionType compressionType = CompressionType.deserialize(headerBuffer.get());
final int dataInDiskBytes = headerBuffer.getInt();
int headerBytes = SEGMENT_HEADER_BASE_BYTES;
if (compressionType != CompressionType.UNCOMPRESSED) {
headerBytes += COMPRESSED_SEGMENT_EXTRA_HEADER_BYTES;
}
final long nextOffset = offset + headerBytes + dataInDiskBytes;
if (nextOffset > analysis.segmentEndOffsetExclusive) {
analysis.segmentParseWarning =
String.format(
Locale.ROOT,
"segment at offset %d exceeds body boundary (%d > %d)",
offset,
nextOffset,
analysis.segmentEndOffsetExclusive);
return;
}
analysis.segmentCount++;
analysis.segmentHeaderBytes += headerBytes;
analysis.segmentPayloadBytes += dataInDiskBytes;
if (compressionType != CompressionType.UNCOMPRESSED) {
analysis.compressedSegmentCount++;
}
offset = nextOffset;
}
if (offset != analysis.segmentEndOffsetExclusive) {
analysis.segmentParseWarning =
String.format(
Locale.ROOT,
"segment parser stopped at %d but expected %d",
offset,
analysis.segmentEndOffsetExclusive);
}
}
private static void parseFooter(final FileChannel channel, final WalFileAnalysis analysis)
throws IOException {
if (analysis.metadataBytes <= 0) {
return;
}
final ByteBuffer metadataBuffer = ByteBuffer.allocate(analysis.metadataBytes);
readFully(channel, metadataBuffer, analysis.footerStartOffset);
metadataBuffer.flip();
if (metadataBuffer.remaining() < Long.BYTES + Integer.BYTES) {
analysis.footerWarning = "metadata buffer is too small";
return;
}
metadataBuffer.getLong();
analysis.firstSearchIndexBytes = Long.BYTES;
final int entryCount = metadataBuffer.getInt();
analysis.entryCount = entryCount;
analysis.entryCountBytes = Integer.BYTES;
analysis.bufferSizeArrayBytes = (long) entryCount * Integer.BYTES;
for (int i = 0; i < entryCount; i++) {
metadataBuffer.getInt();
}
final boolean serializedEmptyV3WithoutMemTableCount =
V3_MAGIC.equals(analysis.version)
&& entryCount == 0
&& metadataBuffer.remaining() == V3_EMPTY_METADATA_REMAINING_WITHOUT_MEMTABLE_COUNT;
if (metadataBuffer.hasRemaining() && !serializedEmptyV3WithoutMemTableCount) {
analysis.memTableCountFieldBytes = Integer.BYTES;
analysis.memTableCount = metadataBuffer.getInt();
analysis.memTableIdsBytes = (long) analysis.memTableCount * Long.BYTES;
for (int i = 0; i < analysis.memTableCount; i++) {
metadataBuffer.getLong();
}
}
if (V3_MAGIC.equals(analysis.version) && metadataBuffer.hasRemaining()) {
if (metadataBuffer.remaining() < Long.BYTES * 2) {
analysis.footerWarning = "V3 metadata is truncated before min/max timestamp range";
return;
}
analysis.minMaxDataTsBytes = Long.BYTES * 2L;
metadataBuffer.getLong();
metadataBuffer.getLong();
final long requiredWriterMetadataBytes =
(long) entryCount * Long.BYTES * 2 + Short.BYTES + Integer.BYTES;
if (metadataBuffer.remaining() < requiredWriterMetadataBytes) {
analysis.footerWarning = "V3 metadata is truncated before writer progress arrays";
return;
}
analysis.physicalTimesBytes = (long) entryCount * Long.BYTES;
for (int i = 0; i < entryCount; i++) {
metadataBuffer.getLong();
}
analysis.defaultWriterIdentityBytes = Short.BYTES;
metadataBuffer.getShort();
analysis.overrideCountFieldBytes = Integer.BYTES;
analysis.overrideCount = metadataBuffer.getInt();
analysis.overrideIndexesBytes = (long) analysis.overrideCount * Integer.BYTES;
analysis.overrideNodeIdsBytes = (long) analysis.overrideCount * Short.BYTES;
for (int i = 0; i < analysis.overrideCount; i++) {
metadataBuffer.getInt();
}
for (int i = 0; i < analysis.overrideCount; i++) {
metadataBuffer.getShort();
}
analysis.localSeqsBytes = (long) entryCount * Long.BYTES;
for (int i = 0; i < entryCount; i++) {
metadataBuffer.getLong();
}
}
analysis.unknownMetadataBytes = metadataBuffer.remaining();
}
private static String detectVersion(final FileChannel channel, final long totalBytes)
throws IOException {
if (totalBytes >= V3_MAGIC.length()
&& readString(channel, 0, V3_MAGIC.length()).equals(V3_MAGIC)) {
return V3_MAGIC;
}
if (totalBytes >= V2_MAGIC.length()
&& readString(channel, 0, V2_MAGIC.length()).equals(V2_MAGIC)) {
return V2_MAGIC;
}
if (totalBytes >= V1_MAGIC.length()
&& readString(channel, totalBytes - V1_MAGIC.length(), V1_MAGIC.length())
.equals(V1_MAGIC)) {
return V1_MAGIC;
}
return "UNKNOWN";
}
private static int getHeadMagicBytes(final String version) {
if (V3_MAGIC.equals(version)) {
return V3_MAGIC.length();
}
if (V2_MAGIC.equals(version)) {
return V2_MAGIC.length();
}
return 0;
}
private static int getTailMagicBytes(final String version) {
if (V3_MAGIC.equals(version)) {
return V3_MAGIC.length();
}
if (V2_MAGIC.equals(version)) {
return V2_MAGIC.length();
}
if (V1_MAGIC.equals(version)) {
return V1_MAGIC.length();
}
return 0;
}
private static boolean hasTrailingMagic(
final FileChannel channel, final long totalBytes, final String version) throws IOException {
final int tailMagicBytes = getTailMagicBytes(version);
if (tailMagicBytes <= 0 || totalBytes < tailMagicBytes) {
return false;
}
return readString(channel, totalBytes - tailMagicBytes, tailMagicBytes).equals(version);
}
private static String readString(final FileChannel channel, final long offset, final int length)
throws IOException {
final ByteBuffer buffer = ByteBuffer.allocate(length);
readFully(channel, buffer, offset);
buffer.flip();
return StandardCharsets.UTF_8.decode(buffer).toString();
}
private static int readInt(final FileChannel channel, final long offset) throws IOException {
final ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
readFully(channel, buffer, offset);
buffer.flip();
return buffer.getInt();
}
private static void readFully(
final FileChannel channel, final ByteBuffer buffer, final long offset) throws IOException {
long position = offset;
while (buffer.hasRemaining()) {
final int bytesRead = channel.read(buffer, position);
if (bytesRead < 0) {
throw new IOException("Unexpected EOF while reading at offset " + position);
}
position += bytesRead;
}
}
private static void printAnalysis(final WalFileAnalysis analysis) {
System.out.println("=== WAL File Layout Analysis ===");
System.out.println("file: " + analysis.file.getAbsolutePath());
System.out.println("version: " + analysis.version);
System.out.println("total: " + formatBytes(analysis.totalBytes));
if (analysis.note != null) {
System.out.println("note: " + analysis.note);
}
System.out.println();
printSection("head magic", analysis.headMagicBytes, analysis.totalBytes);
printSection("segment headers", analysis.segmentHeaderBytes, analysis.totalBytes);
printSection("segment payload", analysis.segmentPayloadBytes, analysis.totalBytes);
printSection("wal end marker", analysis.endMarkerBytes, analysis.totalBytes);
printSection("footer metadata", analysis.metadataBytes, analysis.totalBytes);
printSection("metadata size field", analysis.metadataSizeFieldBytes, analysis.totalBytes);
printSection("tail magic", analysis.tailMagicBytes, analysis.totalBytes);
final long accountedBytes =
analysis.headMagicBytes
+ analysis.segmentHeaderBytes
+ analysis.segmentPayloadBytes
+ analysis.endMarkerBytes
+ analysis.metadataBytes
+ analysis.metadataSizeFieldBytes
+ analysis.tailMagicBytes;
if (analysis.totalBytes >= accountedBytes) {
printSection("unaccounted", analysis.totalBytes - accountedBytes, analysis.totalBytes);
}
System.out.println();
System.out.println(
String.format(
Locale.ROOT,
"segments: total=%d, compressed=%d",
analysis.segmentCount,
analysis.compressedSegmentCount));
if (analysis.segmentParseWarning != null) {
System.out.println("segment warning: " + analysis.segmentParseWarning);
}
if (analysis.metadataBytes <= 0) {
return;
}
System.out.println();
System.out.println("=== Footer Breakdown ===");
printSection("v2-compatible base", analysis.getV2BaseMetadataBytes(), analysis.totalBytes);
if (V3_MAGIC.equals(analysis.version)) {
printSection("v3 extension total", analysis.getV3ExtensionBytes(), analysis.totalBytes);
System.out.println(
String.format(
Locale.ROOT,
"v3 extension share of footer: %s",
formatPercent(analysis.getV3ExtensionBytes(), analysis.metadataBytes)));
printSection(" min/max data ts", analysis.minMaxDataTsBytes, analysis.totalBytes);
printSection(" physicalTimes[]", analysis.physicalTimesBytes, analysis.totalBytes);
printSection(
" default writer identity + override count",
analysis.defaultWriterIdentityBytes + analysis.overrideCountFieldBytes,
analysis.totalBytes);
printSection(" overrideIndexes[]", analysis.overrideIndexesBytes, analysis.totalBytes);
printSection(" overrideNodeIds[]", analysis.overrideNodeIdsBytes, analysis.totalBytes);
printSection(" localSeqs[]", analysis.localSeqsBytes, analysis.totalBytes);
}
if (analysis.unknownMetadataBytes > 0) {
printSection("unknown metadata tail", analysis.unknownMetadataBytes, analysis.totalBytes);
}
System.out.println(
String.format(
Locale.ROOT,
"entries=%d, memTables=%d, overrides=%d",
analysis.entryCount,
analysis.memTableCount,
analysis.overrideCount));
if (analysis.footerWarning != null) {
System.out.println("footer warning: " + analysis.footerWarning);
}
}
private static void printSection(final String name, final long bytes, final long totalBytes) {
System.out.println(
String.format(
Locale.ROOT,
"%-42s %12s %8s",
name + ":",
formatBytes(bytes),
formatPercent(bytes, totalBytes)));
}
private static String formatBytes(final long bytes) {
final long absBytes = Math.abs(bytes);
if (absBytes < 1024L) {
return bytes + " B";
}
if (absBytes < 1024L * 1024L) {
return String.format(Locale.ROOT, "%.2f KiB", bytes / 1024.0d);
}
if (absBytes < 1024L * 1024L * 1024L) {
return String.format(Locale.ROOT, "%.2f MiB", bytes / 1024.0d / 1024.0d);
}
return String.format(Locale.ROOT, "%.2f GiB", bytes / 1024.0d / 1024.0d / 1024.0d);
}
private static String formatPercent(final long bytes, final long totalBytes) {
if (totalBytes <= 0) {
return "N/A";
}
return String.format(Locale.ROOT, "%.2f%%", bytes * 100.0d / totalBytes);
}
private static final class WalFileAnalysis {
private final File file;
private final String version;
private final long totalBytes;
private long headMagicBytes;
private long segmentHeaderBytes;
private long segmentPayloadBytes;
private long endMarkerBytes;
private int metadataBytes;
private long metadataSizeFieldBytes;
private long tailMagicBytes;
private long footerStartOffset;
private long segmentStartOffset;
private long segmentEndOffsetExclusive;
private long segmentRegionBytes;
private int segmentCount;
private int compressedSegmentCount;
private int entryCount;
private int memTableCount;
private int overrideCount;
private long firstSearchIndexBytes;
private long entryCountBytes;
private long bufferSizeArrayBytes;
private long memTableCountFieldBytes;
private long memTableIdsBytes;
private long minMaxDataTsBytes;
private long physicalTimesBytes;
private long localSeqsBytes;
private long defaultWriterIdentityBytes;
private long overrideCountFieldBytes;
private long overrideIndexesBytes;
private long overrideNodeIdsBytes;
private long unknownMetadataBytes;
private String note;
private String segmentParseWarning;
private String footerWarning;
private WalFileAnalysis(final File file, final String version, final long totalBytes) {
this.file = file;
this.version = version;
this.totalBytes = totalBytes;
}
private long getV2BaseMetadataBytes() {
return firstSearchIndexBytes
+ entryCountBytes
+ bufferSizeArrayBytes
+ memTableCountFieldBytes
+ memTableIdsBytes;
}
private long getV3ExtensionBytes() {
return minMaxDataTsBytes
+ physicalTimesBytes
+ defaultWriterIdentityBytes
+ overrideCountFieldBytes
+ overrideIndexesBytes
+ overrideNodeIdsBytes
+ localSeqsBytes;
}
}
}