Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 850f29a1 authored by Jeff Sharkey's avatar Jeff Sharkey Committed by Android (Google) Code Review
Browse files

Merge changes Iac97c342,I52518d52

* changes:
  Enable sendfile() and splice() optimizations.
  Use sendfile() and splice() to speed up copying.
parents f6275e27 274ad550
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.FileUtils;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
@@ -34,6 +35,7 @@ import android.text.TextUtils;

import libcore.io.Streams;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;

@@ -583,7 +585,7 @@ public class Content {
        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            try (ParcelFileDescriptor fd = provider.openFile(null, mUri, "r", null, null)) {
                Streams.copy(new FileInputStream(fd.getFileDescriptor()), System.out);
                FileUtils.copy(fd.getFileDescriptor(), FileDescriptor.out);
            }
        }
    }
@@ -596,7 +598,7 @@ public class Content {
        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            try (ParcelFileDescriptor fd = provider.openFile(null, mUri, "w", null, null)) {
                Streams.copy(System.in, new FileOutputStream(fd.getFileDescriptor()));
                FileUtils.copy(FileDescriptor.in, fd.getFileDescriptor());
            }
        }
    }
+231 −17
Original line number Diff line number Diff line
@@ -16,6 +16,11 @@

package android.os;

import static android.system.OsConstants.SPLICE_F_MORE;
import static android.system.OsConstants.SPLICE_F_MOVE;
import static android.system.OsConstants.S_ISFIFO;
import static android.system.OsConstants.S_ISREG;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.provider.DocumentsContract.Document;
@@ -29,6 +34,7 @@ import android.webkit.MimeTypeMap;

import com.android.internal.annotations.VisibleForTesting;

import libcore.io.IoUtils;
import libcore.util.EmptyArray;

import java.io.BufferedInputStream;
@@ -41,10 +47,12 @@ import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
@@ -81,6 +89,14 @@ public class FileUtils {

    private static final File[] EMPTY = new File[0];

    private static final boolean ENABLE_COPY_OPTIMIZATIONS = true;

    private static final long COPY_CHECKPOINT_BYTES = 524288;

    public interface CopyListener {
        public void onProgress(long progress);
    }

    /**
     * Set owner and mode of of given {@link File}.
     *
@@ -185,6 +201,9 @@ public class FileUtils {
        return false;
    }

    /**
     * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
     */
    @Deprecated
    public static boolean copyFile(File srcFile, File destFile) {
        try {
@@ -195,14 +214,19 @@ public class FileUtils {
        }
    }

    // copy a file from srcFile to destFile, return true if succeed, return
    // false if fail
    /**
     * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
     */
    @Deprecated
    public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
        try (InputStream in = new FileInputStream(srcFile)) {
            copyToFileOrThrow(in, destFile);
        }
    }

    /**
     * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
     */
    @Deprecated
    public static boolean copyToFile(InputStream inputStream, File destFile) {
        try {
@@ -214,28 +238,153 @@ public class FileUtils {
    }

    /**
     * Copy data from a source stream to destFile.
     * Return true if succeed, return false if failed.
     * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
     */
    public static void copyToFileOrThrow(InputStream inputStream, File destFile)
            throws IOException {
    @Deprecated
    public static void copyToFileOrThrow(InputStream in, File destFile) throws IOException {
        if (destFile.exists()) {
            destFile.delete();
        }
        FileOutputStream out = new FileOutputStream(destFile);
        try (FileOutputStream out = new FileOutputStream(destFile)) {
            copy(in, out);
            try {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) >= 0) {
                out.write(buffer, 0, bytesRead);
                Os.fsync(out.getFD());
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
        } finally {
            out.flush();
        }
    }

    public static void copy(File from, File to) throws IOException {
        try (FileInputStream in = new FileInputStream(from);
                FileOutputStream out = new FileOutputStream(to)) {
            copy(in, out);
        }
    }

    public static void copy(InputStream in, OutputStream out) throws IOException {
        copy(in, out, null, null);
    }

    public static void copy(InputStream in, OutputStream out, CopyListener listener,
            CancellationSignal signal) throws IOException {
        if (ENABLE_COPY_OPTIMIZATIONS) {
            if (in instanceof FileInputStream && out instanceof FileOutputStream) {
                copy(((FileInputStream) in).getFD(), ((FileOutputStream) out).getFD(),
                        listener, signal);
            }
        }

        // Worse case fallback to userspace
        copyInternalUserspace(in, out, listener, signal);
    }

    public static void copy(FileDescriptor in, FileDescriptor out) throws IOException {
        copy(in, out, null, null);
    }

    public static void copy(FileDescriptor in, FileDescriptor out, CopyListener listener,
            CancellationSignal signal) throws IOException {
        if (ENABLE_COPY_OPTIMIZATIONS) {
            try {
                out.getFD().sync();
            } catch (IOException e) {
                final StructStat st_in = Os.fstat(in);
                final StructStat st_out = Os.fstat(out);
                if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) {
                    copyInternalSendfile(in, out, listener, signal);
                } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) {
                    copyInternalSplice(in, out, listener, signal);
                }
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
        }

        // Worse case fallback to userspace
        copyInternalUserspace(in, out, listener, signal);
    }

    /**
     * Requires one of input or output to be a pipe.
     */
    @VisibleForTesting
    public static void copyInternalSplice(FileDescriptor in, FileDescriptor out,
            CopyListener listener, CancellationSignal signal) throws ErrnoException {
        long progress = 0;
        long checkpoint = 0;

        long t;
        while ((t = Os.splice(in, null, out, null, COPY_CHECKPOINT_BYTES,
                SPLICE_F_MOVE | SPLICE_F_MORE)) != 0) {
            progress += t;
            checkpoint += t;

            if (checkpoint >= COPY_CHECKPOINT_BYTES) {
                if (signal != null) {
                    signal.throwIfCanceled();
                }
                if (listener != null) {
                    listener.onProgress(progress);
                }
                checkpoint = 0;
            }
        }
    }

    /**
     * Requires both input and output to be a regular file.
     */
    @VisibleForTesting
    public static void copyInternalSendfile(FileDescriptor in, FileDescriptor out,
            CopyListener listener, CancellationSignal signal) throws ErrnoException {
        long progress = 0;
        long checkpoint = 0;

        long t;
        while ((t = Os.sendfile(out, in, null, COPY_CHECKPOINT_BYTES)) != 0) {
            progress += t;
            checkpoint += t;

            if (checkpoint >= COPY_CHECKPOINT_BYTES) {
                if (signal != null) {
                    signal.throwIfCanceled();
                }
                if (listener != null) {
                    listener.onProgress(progress);
                }
                checkpoint = 0;
            }
        }
    }

    @VisibleForTesting
    public static void copyInternalUserspace(FileDescriptor in, FileDescriptor out,
            CopyListener listener, CancellationSignal signal) throws IOException {
        copyInternalUserspace(new FileInputStream(in), new FileOutputStream(out), listener, signal);
    }

    @VisibleForTesting
    public static void copyInternalUserspace(InputStream in, OutputStream out,
            CopyListener listener, CancellationSignal signal) throws IOException {
        long progress = 0;
        long checkpoint = 0;
        byte[] buffer = new byte[8192];

        int t;
        while ((t = in.read(buffer)) != -1) {
            out.write(buffer, 0, t);

            progress += t;
            checkpoint += t;

            if (checkpoint >= COPY_CHECKPOINT_BYTES) {
                if (signal != null) {
                    signal.throwIfCanceled();
                }
                if (listener != null) {
                    listener.onProgress(progress);
                }
                checkpoint = 0;
            }
            out.close();
        }
    }

@@ -797,4 +946,69 @@ public class FileUtils {
        }
        return val * pow;
    }

    @VisibleForTesting
    public static class MemoryPipe extends Thread implements AutoCloseable {
        private final FileDescriptor[] pipe;
        private final byte[] data;
        private final boolean sink;

        private MemoryPipe(byte[] data, boolean sink) throws IOException {
            try {
                this.pipe = Os.pipe();
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
            this.data = data;
            this.sink = sink;
        }

        private MemoryPipe startInternal() {
            super.start();
            return this;
        }

        public static MemoryPipe createSource(byte[] data) throws IOException {
            return new MemoryPipe(data, false).startInternal();
        }

        public static MemoryPipe createSink(byte[] data) throws IOException {
            return new MemoryPipe(data, true).startInternal();
        }

        public FileDescriptor getFD() {
            return sink ? pipe[1] : pipe[0];
        }

        public FileDescriptor getInternalFD() {
            return sink ? pipe[0] : pipe[1];
        }

        @Override
        public void run() {
            final FileDescriptor fd = getInternalFD();
            try {
                int i = 0;
                while (i < data.length) {
                    if (sink) {
                        i += Os.read(fd, data, i, data.length - i);
                    } else {
                        i += Os.write(fd, data, i, data.length - i);
                    }
                }
            } catch (IOException | ErrnoException e) {
                throw new RuntimeException(e);
            } finally {
                if (sink) {
                    SystemClock.sleep(TimeUnit.SECONDS.toMillis(1));
                }
                IoUtils.closeQuietly(fd);
            }
        }

        @Override
        public void close() throws Exception {
            IoUtils.closeQuietly(getFD());
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -160,7 +160,7 @@ public class FileRotator {
                    final File file = new File(mBasePath, name);
                    final FileInputStream is = new FileInputStream(file);
                    try {
                        Streams.copy(is, zos);
                        FileUtils.copy(is, zos);
                    } finally {
                        IoUtils.closeQuietly(is);
                    }
+106 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed 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 android.os;

import static android.os.FileUtils.copyInternalSendfile;
import static android.os.FileUtils.copyInternalSplice;
import static android.os.FileUtils.copyInternalUserspace;

import android.os.FileUtils.MemoryPipe;

import com.google.caliper.BeforeExperiment;
import com.google.caliper.Param;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class FileUtilsBenchmark {
    @Param({"32", "32000", "32000000"})
    private int mSize;

    private File mSrc;
    private File mDest;

    private byte[] mData;

    @BeforeExperiment
    protected void setUp() throws Exception {
        mSrc = new File("/data/local/tmp/src");
        mDest = new File("/data/local/tmp/dest");

        mData = new byte[mSize];

        try (FileOutputStream os = new FileOutputStream(mSrc)) {
            os.write(mData);
        }
    }

    public void timeRegularUserspace(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            try (FileInputStream in = new FileInputStream(mSrc);
                    FileOutputStream out = new FileOutputStream(mDest)) {
                copyInternalUserspace(in.getFD(), out.getFD(), null, null);
            }
        }
    }

    public void timeRegularSendfile(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            try (FileInputStream in = new FileInputStream(mSrc);
                    FileOutputStream out = new FileOutputStream(mDest)) {
                copyInternalSendfile(in.getFD(), out.getFD(), null, null);
            }
        }
    }

    public void timePipeSourceUserspace(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            try (MemoryPipe in = MemoryPipe.createSource(mData);
                    FileOutputStream out = new FileOutputStream(mDest)) {
                copyInternalUserspace(in.getFD(), out.getFD(), null, null);
            }
        }
    }

    public void timePipeSourceSplice(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            try (MemoryPipe in = MemoryPipe.createSource(mData);
                    FileOutputStream out = new FileOutputStream(mDest)) {
                copyInternalSplice(in.getFD(), out.getFD(), null, null);
            }
        }
    }

    public void timePipeSinkUserspace(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            try (FileInputStream in = new FileInputStream(mSrc);
                    MemoryPipe out = MemoryPipe.createSink(mData)) {
                copyInternalUserspace(in.getFD(), out.getFD(), null, null);
            }
        }
    }

    public void timePipeSinkSplice(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            try (FileInputStream in = new FileInputStream(mSrc);
                    MemoryPipe out = MemoryPipe.createSink(mData)) {
                copyInternalSplice(in.getFD(), out.getFD(), null, null);
            }
        }
    }
}
+103 −10
Original line number Diff line number Diff line
@@ -21,16 +21,19 @@ import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.content.Context;
import android.os.FileUtils.MemoryPipe;
import android.provider.DocumentsContract.Document;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import libcore.io.IoUtils;
import libcore.io.Streams;

import com.google.android.collect.Sets;

@@ -40,11 +43,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;

@RunWith(AndroidJUnit4.class)
public class FileUtilsTest {
@@ -56,6 +61,8 @@ public class FileUtilsTest {
    private File mCopyFile;
    private File mTarget;

    private final int[] DATA_SIZES = { 32, 32_000, 32_000_000 };

    private Context getContext() {
        return InstrumentationRegistry.getContext();
    }
@@ -80,7 +87,7 @@ public class FileUtilsTest {

    @Test
    public void testCopyFile() throws Exception {
        stageFile(mTestFile, TEST_DATA);
        writeFile(mTestFile, TEST_DATA);
        assertFalse(mCopyFile.exists());
        FileUtils.copyFile(mTestFile, mCopyFile);
        assertTrue(mCopyFile.exists());
@@ -96,6 +103,83 @@ public class FileUtilsTest {
        assertEquals(s, FileUtils.readTextFile(mCopyFile, 0, null));
    }

    @Test
    public void testCopy_FileToFile() throws Exception {
        for (int size : DATA_SIZES) {
            final File src = new File(mTarget, "src");
            final File dest = new File(mTarget, "dest");

            byte[] expected = new byte[size];
            byte[] actual = new byte[size];
            new Random().nextBytes(expected);
            writeFile(src, expected);

            try (FileInputStream in = new FileInputStream(src);
                    FileOutputStream out = new FileOutputStream(dest)) {
                FileUtils.copy(in, out);
            }

            actual = readFile(dest);
            assertArrayEquals(expected, actual);
        }
    }

    @Test
    public void testCopy_FileToPipe() throws Exception {
        for (int size : DATA_SIZES) {
            final File src = new File(mTarget, "src");

            byte[] expected = new byte[size];
            byte[] actual = new byte[size];
            new Random().nextBytes(expected);
            writeFile(src, expected);

            try (FileInputStream in = new FileInputStream(src);
                    MemoryPipe out = MemoryPipe.createSink(actual)) {
                FileUtils.copy(in.getFD(), out.getFD());
                out.join();
            }

            assertArrayEquals(expected, actual);
        }
    }

    @Test
    public void testCopy_PipeToFile() throws Exception {
        for (int size : DATA_SIZES) {
            final File dest = new File(mTarget, "dest");

            byte[] expected = new byte[size];
            byte[] actual = new byte[size];
            new Random().nextBytes(expected);

            try (MemoryPipe in = MemoryPipe.createSource(expected);
                    FileOutputStream out = new FileOutputStream(dest)) {
                FileUtils.copy(in.getFD(), out.getFD());
            }

            actual = readFile(dest);
            assertArrayEquals(expected, actual);
        }
    }

    @Test
    public void testCopy_PipeToPipe() throws Exception {
        for (int size : DATA_SIZES) {
            byte[] expected = new byte[size];
            byte[] actual = new byte[size];
            new Random().nextBytes(expected);

            try (MemoryPipe in = MemoryPipe.createSource(expected);
                    MemoryPipe out = MemoryPipe.createSink(actual)) {
                FileUtils.copy(in.getFD(), out.getFD());
                out.join();
            }

            assertArrayEquals(expected, actual);
        }
    }

    @Test
    public void testIsFilenameSafe() throws Exception {
        assertTrue(FileUtils.isFilenameSafe(new File("foobar")));
@@ -106,7 +190,7 @@ public class FileUtilsTest {

    @Test
    public void testReadTextFile() throws Exception {
        stageFile(mTestFile, TEST_DATA);
        writeFile(mTestFile, TEST_DATA);

        assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, 0, null));

@@ -127,7 +211,7 @@ public class FileUtilsTest {

    @Test
    public void testReadTextFileWithZeroLengthFile() throws Exception {
        stageFile(mTestFile, TEST_DATA);
        writeFile(mTestFile, TEST_DATA);
        new FileOutputStream(mTestFile).close();  // Zero out the file
        assertEquals("", FileUtils.readTextFile(mTestFile, 0, null));
        assertEquals("", FileUtils.readTextFile(mTestFile, 1, "<>"));
@@ -381,12 +465,21 @@ public class FileUtilsTest {
        file.setLastModified(System.currentTimeMillis() - age);
    }

    private void stageFile(File file, String data) throws Exception {
        FileWriter writer = new FileWriter(file);
        try {
            writer.write(data, 0, data.length());
        } finally {
            writer.close();
    private void writeFile(File file, String data) throws Exception {
        writeFile(file, data.getBytes());
    }

    private void writeFile(File file, byte[] data) throws Exception {
        try (FileOutputStream out = new FileOutputStream(file)) {
            out.write(data);
        }
    }

    private byte[] readFile(File file) throws Exception {
        try (FileInputStream in = new FileInputStream(file);
                ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            Streams.copy(in, out);
            return out.toByteArray();
        }
    }

Loading