diff options
author | Oliver Nguyen <olivernguyen@google.com> | 2020-10-16 22:35:00 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-10-16 22:35:00 +0000 |
commit | a3ef71c8d94a1ae04393a88c433b500def3d840e (patch) | |
tree | 92af6fb182115016d5236dc220baa0dd2f83077a | |
parent | 7a7a61cf3e1719ddf139ac463dd4687c63246544 (diff) | |
parent | 84d82394375f06bdab696ee244a978636f5273c5 (diff) | |
download | jacoco-a3ef71c8d94a1ae04393a88c433b500def3d840e.tar.gz |
Implement delegate that can swap to memory-mapped execution data. am: 4748cfa558 am: 55500ecea4 am: 8da69807af am: 84d8239437
Original change: https://android-review.googlesource.com/c/platform/external/jacoco/+/1455415
Change-Id: I94b694701712e6bcb1d187033937acae4d4737f4
3 files changed, 557 insertions, 3 deletions
diff --git a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/Offline.java b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/Offline.java index ee9d40b1..2350ce55 100644 --- a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/Offline.java +++ b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/Offline.java @@ -11,13 +11,21 @@ *******************************************************************************/ package org.jacoco.agent.rt.internal; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.jacoco.core.data.ExecutionData; +import org.jacoco.core.data.ExecutionDataWriter; +import org.jacoco.core.data.ExecutionDataDelegate; import org.jacoco.core.data.IExecutionData; import org.jacoco.core.data.ExecutionDataStore; +import org.jacoco.core.data.MappedExecutionData; import org.jacoco.core.runtime.AgentOptions; import org.jacoco.core.runtime.RuntimeData; @@ -29,7 +37,8 @@ public final class Offline { // BEGIN android-change // private static final RuntimeData DATA; - private static final Map<Long, IExecutionData> DATA = new HashMap<Long, IExecutionData>(); + private static final Map<Long, ExecutionDataDelegate> DATA = new HashMap<Long, ExecutionDataDelegate>(); + private static FileChannel CHANNEL; // END android-change private static final String CONFIG_RESOURCE = "/jacoco-agent.properties"; @@ -62,9 +71,10 @@ public final class Offline { // return DATA.getExecutionData(Long.valueOf(classid), classname, // probecount).getProbes(); synchronized (DATA) { - IExecutionData entry = DATA.get(classid); + ExecutionDataDelegate entry = DATA.get(classid); if (entry == null) { - entry = new ExecutionData(classid, classname, probecount); + entry = new ExecutionDataDelegate( + classid, classname, probecount, CHANNEL); DATA.put(classid, entry); } else { entry.assertCompatibility(classid, classname, probecount); @@ -72,6 +82,49 @@ public final class Offline { return entry; } } + + /** + * Enables memory-mapped execution data and converts existing + * {@link ExecutionDataDelegate}s. + */ + public static void enableMemoryMappedData() { + try { + prepareFile(getPid()); + for (ExecutionDataDelegate data : DATA.values()) { + data.convert(CHANNEL); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates the output file that will be mapped for probe data. + */ + private static void prepareFile(int pid) throws IOException { + // Write header information to the file. + ByteBuffer headerBuffer = ByteBuffer.allocate(5); + headerBuffer.put(ExecutionDataWriter.BLOCK_HEADER); + headerBuffer.putChar(ExecutionDataWriter.MAGIC_NUMBER); + headerBuffer.putChar(ExecutionDataWriter.FORMAT_VERSION); + headerBuffer.flip(); + + // If this file already exists (due to pid re-usage), the previous coverage data + // will be lost when the file is overwritten. + File outputFile = new File("/data/misc/trace/jacoco-" + pid + ".mm.ec"); + CHANNEL = new RandomAccessFile(outputFile, "rw").getChannel(); + synchronized (CHANNEL) { + CHANNEL.write(headerBuffer); + } + } + + /** + * Helper function to determine the pid of this process. + */ + private static int getPid() throws IOException { + // Read /proc/self and resolve it to obtain its pid. + return Integer.parseInt(new File("/proc/self").getCanonicalFile().getName()); + } // END android-change /** diff --git a/org.jacoco.core/src/org/jacoco/core/data/ExecutionDataDelegate.java b/org.jacoco.core/src/org/jacoco/core/data/ExecutionDataDelegate.java new file mode 100644 index 00000000..cc6b3cc8 --- /dev/null +++ b/org.jacoco.core/src/org/jacoco/core/data/ExecutionDataDelegate.java @@ -0,0 +1,197 @@ +// BEGIN android-change +package org.jacoco.core.data; + +import java.io.IOException; +import java.nio.channels.FileChannel; + +/** + * Class that delegates calls to {@link IExecutionData} interface methods to another instance. + * Also has the ability to convert the underlying implementation to a {@link MappedExecutionData} + * instance. + */ +public class ExecutionDataDelegate implements IExecutionData { + private IExecutionData delegate; + + public ExecutionDataDelegate( + final long id, final String name, final int probeCount, final FileChannel channel) { + if ((channel != null) && canMapData(name)) { + try { + delegate = new MappedExecutionData(id, name, probeCount, channel); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + delegate = new ExecutionData(id, name, probeCount); + } + } + + /** + * Converts the existing delegate into a {@link MappedExecutionData} instance if it is not + * already one, and copies any probe data into the new instance. + */ + public synchronized void convert(final FileChannel channel) throws IOException { + if (!(delegate instanceof MappedExecutionData) && canMapData(getName())) { + IExecutionData newDelegate = new MappedExecutionData( + getId(), getName(), getProbeCount(), channel); + newDelegate.merge(delegate); + delegate = newDelegate; + } + } + + /** + * Determines if a class can be converted to memory-mapped. + */ + private static boolean canMapData(final String name) { + if (name.startsWith("android/app/") + || name.startsWith("android/os/") + || name.startsWith("com/android/internal/util/") + || name.startsWith("java/")) { + return false; + } + return true; + } + + /** + * Return the unique identifier for this class. The identifier is the CRC64 + * checksum of the raw class file definition. + * + * @return class identifier + */ + public long getId() { + return delegate.getId(); + } + + /** + * The VM name of the class. + * + * @return VM name + */ + public String getName() { + return delegate.getName(); + } + + /** + * The number of instrumentation probes for this class. + * + * @return number of probes + */ + public int getProbeCount() { + return delegate.getProbeCount(); + } + + /** + * Returns a copy of the probe data as a boolean array. + * + * Changes to the returned array will not be reflected in the execution data. + * + * @return copy of the probe data + */ + public boolean[] getProbesCopy() { + return delegate.getProbesCopy(); + } + + /** + * Sets all probes to <code>false</code>. + */ + public void reset() { + delegate.reset(); + } + + /** + * Checks whether any probe has been hit. + * + * @return <code>true</code>, if at least one probe has been hit + */ + public boolean hasHits() { + return delegate.hasHits(); + } + + /** + * Merges the given execution data into the probe data of this object. I.e. + * a probe entry in this object is marked as executed (<code>true</code>) if + * this probe or the corresponding other probe was executed. So the result + * is + * + * <pre> + * A or B + * </pre> + * + * The probe array of the other object is not modified. + * + * @param other + * execution data to merge + */ + public void merge(final IExecutionData other) { + delegate.merge(other); + } + + /** + * Merges the given execution data into the probe data of this object. A + * probe in this object is set to the value of <code>flag</code> if the + * corresponding other probe was executed. For <code>flag==true</code> this + * corresponds to + * + * <pre> + * A or B + * </pre> + * + * For <code>flag==false</code> this can be considered as a subtraction + * + * <pre> + * A and not B + * </pre> + * + * The probe array of the other object is not modified. + * + * @param other + * execution data to merge + * @param flag + * merge mode + */ + public void merge(final IExecutionData other, boolean flag) { + delegate.merge(other, flag); + } + + /** + * Asserts that this execution data object is compatible with the given + * parameters. The purpose of this check is to detect a very unlikely class + * id collision. + * + * @param id + * other class id, must be the same + * @param name + * other name, must be equal to this name + * @param probecount + * probe data length, must be the same as for this data + * @throws IllegalStateException + * if the given parameters do not match this instance + */ + public void assertCompatibility(final long id, final String name, final int probeCount) + throws IllegalStateException { + delegate.assertCompatibility(id, name, probeCount); + } + + /** + * Returns the execution data probe for a given index. A value of + * <code>true</code> indicates that the corresponding probe was + * executed. + * + * @param index the probe's index to look up + * + * @return probe data + */ + public boolean getProbe(final int index) { + return delegate.getProbe(index); + } + + /** + * Sets the execution data probe at the given index to <code>true</code>. + * + * @param index the probe's index to set + * @param value the value to set the probe to + */ + public void setProbe(final int index) { + delegate.setProbe(index); + } + } + // END android-change diff --git a/org.jacoco.core/src/org/jacoco/core/data/MappedExecutionData.java b/org.jacoco.core/src/org/jacoco/core/data/MappedExecutionData.java new file mode 100644 index 00000000..7e74d17e --- /dev/null +++ b/org.jacoco.core/src/org/jacoco/core/data/MappedExecutionData.java @@ -0,0 +1,304 @@ +// BEGIN android-change +package org.jacoco.core.data; + +import static java.lang.String.format; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; + +import org.jacoco.core.data.ExecutionDataWriter; + +/** + * Memory-mapped execution data implementation. + * + * Stores the probe data in a section of a memory-mapped file, so that it will be available without + * requiring a flush of the coverage data. + */ +public final class MappedExecutionData implements IExecutionData { + private final long id; + private final String name; + private final int probeCount; + + // Stores the memory-mapped byte buffer containing the probe data, packed as bits. + private MappedByteBuffer probeBuffer; + + /** + * Creates the mapped execution data. + */ + public MappedExecutionData( + final long id, final String name, final int probeCount, final FileChannel channel) + throws IOException { + this.id = id; + this.name = name; + this.probeCount = probeCount; + + createMemoryMappedProbeArray(id, name, probeCount, channel); + } + + public MappedExecutionData( + final long id, + final String name, + final boolean[] probes, + final FileChannel channel) throws IOException { + this.id = id; + this.name = name; + this.probeCount = probes.length; + + createMemoryMappedProbeArray(id, name, probes.length, channel); + for (int i = 0; i < probes.length; i++) { + if (probes[i]) { + setProbe(i); + } + } + } + + /** + * Maps a section of the already-opened file for the probe data. + * + * @param id the class id + * @param name the VM class name + * @param probeCount the number of probes for this class + */ + private void createMemoryMappedProbeArray( + final long id, final String name, final int probeCount, final FileChannel channel) + throws IOException { + synchronized (channel) { + int byteCount = (probeCount + 7) / 8; + + // Write the ExecutionData block info. + ByteBuffer execDataBuffer = ByteBuffer.allocate(11); + execDataBuffer.put(ExecutionDataWriter.BLOCK_EXECUTIONDATA); + execDataBuffer.putLong(id); + execDataBuffer.putShort((short) name.length()); + execDataBuffer.flip(); + channel.write(execDataBuffer); + channel.write(ByteBuffer.wrap(name.getBytes(Charset.forName("UTF-8")))); + + // Write the probe info and map part of this file for the probe data. + channel.write(toVarIntByteBuffer(probeCount)); + probeBuffer = channel.map(FileChannel.MapMode.READ_WRITE, channel.position(), byteCount); + channel.position(channel.position() + byteCount); + } + } + + /** + * Writes a variable-length int to a {@link ByteBuffer}. + * + * @param value the value to write + * @return a ByteBuffer that can be used to write to a FileChannel containing the value + */ + private ByteBuffer toVarIntByteBuffer(int value) { + ByteBuffer buffer = ByteBuffer.allocate(5); + if (value == 0) { + buffer.put((byte) 0); + } else { + while (value > 0) { + if ((value & 0xFFFFFF80) == 0) { + buffer.put((byte) value); + } else { + buffer.put((byte) (0x80 | (value & 0x7F))); + } + value >>>= 7; + } + } + buffer.flip(); + return buffer; + } + + /** + * Return the unique identifier for this class. The identifier is the CRC64 + * checksum of the raw class file definition. + * + * @return class identifier + */ + public long getId() { + return id; + } + + /** + * The VM name of the class. + * + * @return VM name + */ + public String getName() { + return name; + } + + /** + * The number of instrumentation probes for this class. + * + * @return number of probes + */ + public int getProbeCount() { + return probeCount; + } + + /** + * Returns a copy of the probe data as a boolean array. + * + * Changes to the returned array will not be reflected in the execution data. + * + * @return copy of the probe data + */ + public boolean[] getProbesCopy() { + final int bytes = (probeCount + 7) / 8; + boolean[] probes = new boolean[probeCount]; + for (int index = 0; index < probeCount; index += 8) { + byte byteProbe = probeBuffer.get(index / 8); + for (int bit = 0; (bit < 8) && ((index + bit) < probeCount); bit++) { + probes[index + bit] = ((byteProbe & 0x1) > 0); + byteProbe >>>= 1; + } + } + return probes; + } + + /** + * Sets all probes to <code>false</code>. + */ + public void reset() { + final int bytes = (probeCount + 7) / 8; + synchronized (probeBuffer) { + for (int i = 0; i < bytes; i++) { + probeBuffer.put(i, (byte) 0); + } + } + } + + /** + * Checks whether any probe has been hit. + * + * @return <code>true</code>, if at least one probe has been hit + */ + public boolean hasHits() { + final int bytes = (probeCount + 7) / 8; + synchronized (probeBuffer) { + for (int i = 0; i < bytes; i++) { + if (probeBuffer.get(i) > 0) { + return true; + } + } + } + return false; + } + + /** + * Merges the given execution data into the probe data of this object. I.e. + * a probe entry in this object is marked as executed (<code>true</code>) if + * this probe or the corresponding other probe was executed. So the result + * is + * + * <pre> + * A or B + * </pre> + * + * The probe array of the other object is not modified. + * + * @param other + * execution data to merge + */ + public void merge(final IExecutionData other) { + merge(other, true); + } + + /** + * Merges the given execution data into the probe data of this object. A + * probe in this object is set to the value of <code>flag</code> if the + * corresponding other probe was executed. For <code>flag==true</code> this + * corresponds to + * + * <pre> + * A or B + * </pre> + * + * For <code>flag==false</code> this can be considered as a subtraction + * + * <pre> + * A and not B + * </pre> + * + * The probe array of the other object is not modified. + * + * @param other + * execution data to merge + * @param flag + * merge mode + */ + public void merge(final IExecutionData other, final boolean flag) { + synchronized (probeBuffer) { + for (int i = 0; i < probeCount; i++) { + if (other.getProbe(i)) { + setProbe(i); + } + } + } + } + + /** + * Asserts that this execution data object is compatible with the given + * parameters. The purpose of this check is to detect a very unlikely class + * id collision. + * + * @param id + * other class id, must be the same + * @param name + * other name, must be equal to this name + * @param probecount + * probe data length, must be the same as for this data + * @throws IllegalStateException + * if the given parameters do not match this instance + */ + public void assertCompatibility(final long id, final String name, final int probeCount) + throws IllegalStateException { + if (this.id != id) { + throw new IllegalStateException(format( + "Different ids (%016x and %016x).", Long.valueOf(this.id), + Long.valueOf(id))); + } + if (!this.name.equals(name)) { + throw new IllegalStateException(format( + "Different class names %s and %s for id %016x.", this.name, + name, Long.valueOf(id))); + } + if (this.probeCount != probeCount) { + throw new IllegalStateException(format( + "Incompatible execution data for class %s with id %016x.", + name, Long.valueOf(id))); + } + } + + /** + * Returns the execution data probe for a given index. A value of + * <code>true</code> indicates that the corresponding probe was + * executed. + * + * @param index the probe's index to look up + * + * @return probe data + */ + public boolean getProbe(final int index) { + int offset = index / 8; + int bit = 1 << (index % 8); + return (probeBuffer.get(offset) & bit) != 0; + } + + /** + * Sets the execution data probe at the given index to <code>true</code>. + * + * @param index the probe's index to set + */ + public void setProbe(final int index) { + int offset = index / 8; + int bit = 1 << (index % 8); + byte currentValue = probeBuffer.get(offset); + if ((currentValue & bit) == 0) { + synchronized (probeBuffer) { + probeBuffer.put(offset, (byte) (probeBuffer.get(offset) | bit)); + } + } + } +} +// END android-change |