Mobile Development 25 min read

Dex DebugInfo Line Number Optimization in Android Applications

The article explains how Baidu’s Dex line-number optimization replaces original line numbers with compact PC-based mappings, unifies debug_info_items, and uses a server-side mapping file to shrink DebugInfo by roughly 8% of the Dex size while keeping stack-trace line numbers fully recoverable.

Baidu App Technology
Baidu App Technology
Baidu App Technology
Dex DebugInfo Line Number Optimization in Android Applications

In the previous article we briefly introduced the basic ideas of Android package size optimization and its various optimization items. This article focuses on line‑number optimization of Dex DebugInfo, aiming to reduce the size of DebugInfo while keeping the original debugging information traceable.

We refer to existing line‑number optimization schemes from the industry (e.g., Alipay, R8) that replace the line‑number set with a PC set, achieving maximum reuse of DebugInfo and solving the overlapping line‑number range problem for overloaded methods, while providing a complete original line‑number retrace solution.

As shown in Figure 1‑1, the DebugInfo of two methods is visualized. The mapping between instruction set and original line numbers is exported to a mapping file and uploaded to the server for later retrace processing. After mapping, the DebugInfo of both methods becomes identical, achieving a reusable state.

Next we will detail DebugInfo analysis, compare existing solutions, present Baidu APP's optimization scheme, and discuss the benefits.

2.1 Dex DebugInfo

In the Dex file format, DebugInfo resides in the data section and consists of a series of debug_info_item structures.

Typically, each debug_info_item corresponds one‑to‑one with a class method. The reference relationship in Dex is shown in Figure 2‑2, where offsets (x_off) determine the location of reference areas.

The debug_info_item structure consists of a header and a series of debug_event s. The header contains the method start line, the number of method parameters, and parameter names. The debug_events act as a state machine, recording PC pointers and line‑number deltas.

Common debug_event types are listed in the table below:

Name

Value

Parameter

Description

DBG_END_SEQUENCE

0x00

None

End of debug_info_item, cannot be modified

DBG_ADVANCE_PC

0x01

pcDelta

Contains only PC offset

DBG_ADVANCE_LINE

0x02

lineDelta

Contains only line offset

Special Opcodes

[0x0a,0xff]

None

PC and line offsets can be derived from the value

The conversion formula for Special Opcodes is:

DBG_FIRST_SPECIAL = 0x0a  // smallest special opcode
DBG_LINE_BASE   = -4      // smallest line number increment
DBG_LINE_RANGE  = 15      // number of line increments represented
adjusted_opcode = opcode - DBG_FIRST_SPECIAL
line += DBG_LINE_BASE + (adjusted_opcode % DBG_LINE_RANGE)
address += (adjusted_opcode / DBG_LINE_RANGE)

2.2 DebugInfo Usage Scenarios

DebugInfo is commonly used for breakpoint debugging and stack trace location (including crash, ANR, memory analysis, etc.). When an exception occurs, the JVM obtains a StackTrace that stores ArtMethod objects and corresponding PC values without line numbers. The native method nativeGetStackTrace converts this to a StackTraceElement[] , which includes source file and line number (see Figure 2‑4).

The relevant code path is shown below (excerpt from ART source):

// art/runtime/native/java_lang_Throwable.cc
static jobjectArray Throwable_nativeGetStackTrace(JNIEnv* env, jclass, jobject javaStackState) {
  ...
  ScopedFastNativeObjectAccess soa(env);
  return Thread::InternalStackTraceToStackTraceElementArray(soa, javaStackState);
}
// art/runtime/thread.cc
jobjectArray Thread::InternalStackTraceToStackTraceElementArray(const ScopedObjectAccessAlreadyRunnable& soa,
    jobject internal, jobjectArray output_array, int* stack_depth) {
  ...
  for (uint32_t i = 0; i < static_cast
(depth); ++i) {
    ObjPtr
> decoded_traces = ...;
    const ObjPtr
method_trace = ObjPtr
::DownCast(decoded_traces->Get(0));
    ArtMethod* method = method_trace->GetElementPtrSize
(i, kRuntimePointerSize);
    uint32_t dex_pc = method_trace->GetElementPtrSize
(i + static_cast
(method_trace->GetLength()) / 2, kRuntimePointerSize);
    const ObjPtr
obj = CreateStackTraceElement(soa, method, dex_pc);
    ...
  }
  return result;
}
static ObjPtr
CreateStackTraceElement(const ScopedObjectAccessAlreadyRunnable& soa,
    ArtMethod* method, uint32_t dex_pc) {
  int32_t line_number = method->GetLineNumFromDexPC(dex_pc);
  ...
}

From the GetLineNumForPc method we can see that the VM traverses the corresponding DebugInfo to retrieve the original line number. In our scheme we set all pcDelta to 1, which slightly increases traversal length but the processing is trivial, so performance impact is negligible.

3 Existing Optimization Schemes

3.1 Extreme Optimization

DebugInfo can be completely removed, which saves size but makes stack traces lose line numbers (they show –1). This is acceptable only for highly stable apps where debugging is rarely needed.

Java compilers and obfuscation tools provide options to omit DebugInfo attributes (SourceFile, SourceDebugExtension, LineNumberTable, LocalVariableTable) from class files (see Figure 3‑1).

3.2 Mapping Optimization

Instead of removing DebugInfo, we keep the DebugInfo region but change the 1‑to‑1 relationship between methods and debug_info_item to an N‑to‑1 reuse relationship. This reduces the number of debug_info_item structures and thus the size, while a mapping file records the before‑and‑after relationship for later retrace. Alipay, R8, and Baidu APP all use this approach.

Two debug_info_item s are considered equal if their start line, parameters, and events are identical. Since we do not need method parameters for stack traces, only start line and debug_events need to be unified.

// Pseudo‑code for equality check
public boolean equals(DebugInfoItem other) {
    return this.startLine == other.startLine &&
           this.parameters.equals(other.parameters) &&
           this.events.equals(other.events);
}

Similarly, debug_event equality is defined as:

public boolean equals(DebugEvent other) {
    return this.type == other.type && this.value == other.value;
}

To achieve reuse we control the following variables: startLine , number of debug_events , event type, lineDelta , and pcDelta (opcode is derived from the two deltas).

3.3 Alipay Line‑Number Optimization

Alipay proposes two schemes:

Extract all DebugInfo into a separate debugInfo.dex file, removing DebugInfo from the main APK.

When a crash occurs, hook Throwable to obtain the instruction‑set line number and upload it.

The performance platform uses the uploaded debugInfo.dex to map the instruction line number back to the original source line.

This approach works only for Throwable scenarios and requires handling different JVM stack‑trace structures.

3.4 R8 Line‑Number Optimization

R8 keeps LineNumberTable and modifies debug_info_item as follows:

startLine : default 1; for overloaded methods the later method’s startLine becomes previous endLine +1.

lineDelta : default 1.

Although this enables some reuse, the number of debug_events and pcDelta remain uncontrolled, limiting reuse.

4 Baidu APP Dex Line‑Number Optimization Scheme

4.1 Client‑Side Optimization

Variables are controlled as follows:

startLine : default 100000 (much larger than R8’s default of 1) to avoid overlap in hot‑fix or plugin scenarios.

debug_event : besides the start/end events, all other events are special opcodes with pcDelta=lineDelta=1 . The number of events equals the method’s instruction count.

pcDelta : first special opcode is 0, others are 1.

lineDelta : same as pcDelta , i.e., 1.

Figure 4‑2 illustrates the ideal line‑number interval distribution, and Figures 4‑3/4‑4 show the mapping between instruction count and debug_event quantity.

4.2 Performance Platform Line‑Number Retrace

After optimization, the app reports virtual line numbers. The performance platform receives crash/ANR data, looks up the corresponding mapping file (uploaded during release), and converts virtual line numbers back to real source lines. The architecture consists of a streaming computation service, a multi‑level cache (in‑memory → Redis → Table), and a mapping‑file parsing service.

Mapping files have the format:

ClassName:
    methodDescriptor:
        [mappedStart-mappedEnd] -> originalLine
        ...

Example:

com.baidu.searchbox.Application:
    void onCreate(android.os.Bundle):
        [1000-1050] -> 20
        [1051-2000] -> 22
    void onCreate():
        [3000-3020] -> 30
        [3021-3033] -> 31

The streaming service caches hot mapping entries in operator memory, falls back to Redis, and finally to persistent storage, ensuring millisecond‑level lookup latency.

5 Summary

This article introduced the structure of Dex DebugInfo, analyzed its usage, compared several optimization schemes, and detailed Baidu APP’s line‑number optimization and retrace workflow. By controlling startLine , pcDelta , lineDelta , and the number of debug_event s, Baidu achieved a significant reduction in Dex size (approximately 8% of Dex, 3.04 MB of the APK) while preserving accurate stack‑trace information through a robust server‑side mapping service.

performanceAndroidbytecodeDexDebugInfoLineNumberOptimization
Baidu App Technology
Written by

Baidu App Technology

Official Baidu App Tech Account

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.