1 /* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 ==============================================================================*/
15 #include "tensorflow/lite/experimental/acceleration/mini_benchmark/runner.h"
16
17 // Implementation notes and rationale:
18 //
19 // This class's primary client is the mini-benchmark. The mini-benchmark tries
20 // out different acceleration configurations for TFLite. The acceleration may
21 // hang or crash due to driver bugs. Running in a separate process isolates the
22 // application from these hangs or crashes.
23 //
24 // The separate process is implemented in native code. The main alternative
25 // would be to use a separate service process at the Android application
26 // level. The most important drawbacks of that approach would have been
27 // manifest merge issues in tests (see for example b/158142805) and the need for
28 // all applications to explicitly integrate the mini-benchmark at the app level.
29 //
30 // The native code uses popen(3) for the separate process. This is one of the
31 // few officially supported ways of safely using fork(2) on Android (See
32 // b/129156634 for a discussion on popen use in Android studio device-side, see
33 // https://groups.google.com/g/android-platform/c/80jr-_A-9bU for discussion on
34 // fork in general).
35 //
36 // The native process executes a small helper binary that then dynamically
37 // loads the shared library that tflite code lives in. This allows for minimal
38 // code size as only one copy of the tflite code is needed.
39 //
40 // The 8kb helper binary is extracted into an app data folder On P- we execute
41 // it directly. On Q+ this is no longer allowed (see b/112357170) but we can
42 // use /system/bin/linker(64) as a trampoline. (See also Chrome's documentation
43 // for a similar problem:
44 // https://chromium.googlesource.com/chromium/src/+/master/docs/android_native_libraries.md#crashpad-packaging).
45 // Using /system/bin/linker(64) does not work before Android Q (See
46 // b/112050209).
47 //
48 // The shared library where the code to be called lives is a JNI library that
49 // contains tflite. We detect the path to that library with dladdr(3). This way
50 // differences in the way the JNI libraries are bundled, named or delivered is
51 // automatically handled. The downside is that dladdr is broken on Android 23
52 // for JNI libraries that are not extracted from the apk (See b/37069572). If
53 // this becomes a serious issue we can try to parse /proc/self/maps and the apk
54 // zip directory to figure out the name. The alternative where the caller needs
55 // to pass in the library name requires more integration work at the packaging
56 // (app or SDK) level.
57 //
58 // The methods in this class return detailed error codes, so that telemetry can
59 // be used to detect issues in production without using strings which can be
60 // hard to make privacy-compliant.
61
62 #ifdef __ANDROID__
63 #include <dlfcn.h>
64 #endif
65 #include <stdio.h>
66 #include <stdlib.h>
67 #include <sys/stat.h>
68 #ifndef _WIN32
69 #include <unistd.h>
70 #endif
71
72 #include <cstdlib>
73 #include <fstream>
74 #include <sstream>
75 #include <string>
76 #include <thread> // NOLINT(build/c++11)
77 #include <vector>
78
79 #include "flatbuffers/flatbuffers.h" // from @flatbuffers
80 #include "tensorflow/lite/experimental/acceleration/compatibility/android_info.h"
81 #include "tensorflow/lite/experimental/acceleration/mini_benchmark/status_codes.h"
82 #include "tensorflow/lite/minimal_logging.h"
83
84 #ifdef __ANDROID__
85 #include "tensorflow/lite/experimental/acceleration/mini_benchmark/embedded_runner_executable.h"
86 #endif // __ANDROID__
87
88 namespace tflite {
89 namespace acceleration {
90 namespace {
91 std::string ShellEscape(const std::string& src);
92 } // namespace
93
ProcessRunner(const std::string & temporary_path,const std::string & function_name,int (* function_pointer)(int argc,char ** argv))94 ProcessRunner::ProcessRunner(const std::string& temporary_path,
95 const std::string& function_name,
96 int (*function_pointer)(int argc, char** argv))
97 : temporary_path_(temporary_path),
98 function_name_(function_name),
99 function_pointer_(reinterpret_cast<void*>(function_pointer)) {}
100
Init()101 MinibenchmarkStatus ProcessRunner::Init() {
102 if (!function_pointer_) {
103 return kMinibenchmarkPreconditionNotMet;
104 }
105 #ifndef __ANDROID__
106 return kMinibenchmarkSuccess;
107 #else
108 tflite::acceleration::AndroidInfo android_info;
109 if (!tflite::acceleration::RequestAndroidInfo(&android_info).ok()) {
110 return kMinibenchmarkRequestAndroidInfoFailed;
111 }
112 if (android_info.android_sdk_version.length() < 2 ||
113 android_info.android_sdk_version < "23") {
114 // The codepaths have only been tested on 23+.
115 return kMinibenchmarkUnsupportedPlatform;
116 }
117
118 // Find name of this shared object.
119 std::string soname;
120 Dl_info dl_info;
121 int status = dladdr(function_pointer_, &dl_info);
122 if (status != 0) {
123 if (dl_info.dli_fname) {
124 soname = dl_info.dli_fname;
125 } else {
126 return kMinibenchmarkDliFnameWasNull;
127 }
128 } else {
129 return kMinibenchmarkDladdrReturnedZero;
130 }
131 // Check for b/37069572 - on SDK level 23 dli_fname may not contain the
132 // library name, only the apk. (See comment at top of file).
133 if (soname.size() >= 4 && soname.substr(soname.size() - 4) == ".apk") {
134 return kMinibenchmarkDliFnameHasApkNameOnly;
135 }
136
137 // Construct path to runner, extracting the helper binary if needed.
138 std::string runner_path;
139 // TODO(b/172541832): handle multiple concurrent callers.
140 runner_path = temporary_path_ + "/runner";
141 (void)unlink(runner_path.c_str());
142 std::string runner_contents(
143 reinterpret_cast<const char*>(g_tflite_acceleration_embedded_runner),
144 g_tflite_acceleration_embedded_runner_len);
145 std::ofstream f(runner_path, std::ios::binary);
146 if (!f.is_open()) {
147 return kMinibenchmarkCouldntOpenTemporaryFileForBinary;
148 }
149 f << runner_contents;
150 f.close();
151 if (chmod(runner_path.c_str(), 0500) != 0) {
152 return kMinibenchmarkCouldntChmodTemporaryFile;
153 }
154 runner_path = ShellEscape(runner_path);
155 if (android_info.android_sdk_version >= "29") {
156 // On 29+ we need to use /system/bin/linker to load the binary from the app,
157 // as exec()ing writable files was blocked for security. (See comment at top
158 // of file).
159 #if defined(__arm__) || defined(__i386__)
160 std::string linker_path = "/system/bin/linker";
161 #else
162 std::string linker_path = "/system/bin/linker64";
163 #endif
164 runner_path = linker_path + " " + runner_path;
165 }
166
167 // Commit.
168 runner_path_ = runner_path;
169 soname_ = soname;
170 return kMinibenchmarkSuccess;
171 #endif // !__ANDROID__
172 }
173
Run(flatbuffers::FlatBufferBuilder * model,const std::vector<std::string> & args,std::string * output,int * exitcode,int * signal)174 MinibenchmarkStatus ProcessRunner::Run(flatbuffers::FlatBufferBuilder* model,
175 const std::vector<std::string>& args,
176 std::string* output, int* exitcode,
177 int* signal) {
178 #ifdef _WIN32
179 return kMinibenchmarkUnsupportedPlatform;
180 #else // !_WIN32
181 if (!output || !exitcode) {
182 return kMinibenchmarkPreconditionNotMet;
183 }
184 int benchmark_process_status = 0;
185 #ifndef __ANDROID__
186 if (function_pointer_) {
187 benchmark_process_status = RunInprocess(model, args);
188 } else {
189 return kMinibenchmarkPreconditionNotMet;
190 }
191 #else
192 if (runner_path_.empty()) {
193 return kMinibenchmarkPreconditionNotMet;
194 }
195 // runner_path_ components are escaped earlier.
196 std::string cmd = runner_path_ + " " + ShellEscape(soname_) + " " +
197 ShellEscape(function_name_);
198
199 // If model is not null, open a pipe() and add pipe model path as cmdline
200 // argv[3]. If model is null, argv[0] should be the model path.
201 int pipe_fds[2];
202 if (model != nullptr) {
203 if (pipe(pipe_fds) < 0) {
204 *exitcode = errno;
205 return kMinibenchmarkPipeFailed;
206 }
207 std::string pipe_model_path = absl::StrCat(
208 "pipe:", pipe_fds[0], ":", pipe_fds[1], ":", model->GetSize());
209 cmd = cmd + " " + ShellEscape(pipe_model_path);
210 }
211
212 // Add the rest of the cmdline args.
213 for (const auto& arg : args) {
214 cmd = cmd + " " + ShellEscape(arg);
215 }
216
217 FILE* f = popen(cmd.c_str(), "r");
218 if (!f) {
219 *exitcode = errno;
220 return kMinibenchmarkPopenFailed;
221 }
222
223 // Write model to MiniBenchmark process.
224 if (model != nullptr) {
225 close(pipe_fds[0]);
226 int written_bytes = 0;
227 int remaining_bytes = model->GetSize();
228 uint8_t* buffer = model->GetBufferPointer();
229 while (remaining_bytes > 0 &&
230 (written_bytes = write(pipe_fds[1], buffer, remaining_bytes)) > 0) {
231 remaining_bytes -= written_bytes;
232 buffer += written_bytes;
233 }
234 close(pipe_fds[1]);
235 if (written_bytes <= 0 || remaining_bytes > 0) {
236 *exitcode = errno;
237 return kMinibenchmarkPipeFailed;
238 }
239 }
240
241 std::vector<char> buffer(4 * 1024, 0);
242 ssize_t length;
243 std::string ret;
244 do {
245 length = fread(buffer.data(), 1, buffer.size(), f);
246 ret = ret + std::string(buffer.data(), length);
247 } while (length == buffer.size());
248 *output = ret;
249 benchmark_process_status = pclose(f);
250 #endif // !__ANDROID
251 if (WIFEXITED(benchmark_process_status)) {
252 *exitcode = WEXITSTATUS(benchmark_process_status);
253 *signal = 0;
254 if (*exitcode == kMinibenchmarkSuccess) {
255 return kMinibenchmarkSuccess;
256 }
257 } else if (WIFSIGNALED(benchmark_process_status)) {
258 *exitcode = 0;
259 *signal = WTERMSIG(benchmark_process_status);
260 }
261 return kMinibenchmarkCommandFailed;
262 #endif // _WIN32
263 }
264
265 #ifndef __ANDROID__
266
267 #ifndef __W_EXITCODE // Mac
268 #define __W_EXITCODE(ret, sig) ((ret) << 8 | (sig))
269 #endif
270
RunInprocess(flatbuffers::FlatBufferBuilder * model,const std::vector<std::string> & user_args)271 int ProcessRunner::RunInprocess(flatbuffers::FlatBufferBuilder* model,
272 const std::vector<std::string>& user_args) {
273 std::vector<std::string> args_string;
274 args_string.push_back("inprocess");
275 args_string.push_back("inprocess");
276 args_string.push_back(function_name_);
277
278 std::thread write_thread;
279 if (model != nullptr) {
280 int pipe_fds[2];
281 if (pipe(pipe_fds) < 0) {
282 return __W_EXITCODE(kMinibenchmarkPipeFailed, 0);
283 }
284
285 // Add pipe_model_path when model is not null.
286 // Model loader won't close the write file descriptor when it's -1.
287 args_string.push_back(
288 absl::StrCat("pipe:", pipe_fds[0], ":-1:", model->GetSize()));
289
290 // When running MiniBenchmark in-process, we start a separate thread for
291 // writing to pipe.
292 write_thread = std::thread([pipe_fds, model]() {
293 int written_bytes = 0;
294 int remaining_bytes = model->GetSize();
295 uint8_t* buffer = model->GetBufferPointer();
296 while (remaining_bytes > 0 &&
297 (written_bytes = write(pipe_fds[1], buffer, remaining_bytes)) >
298 0) {
299 remaining_bytes -= written_bytes;
300 buffer += written_bytes;
301 }
302 close(pipe_fds[1]);
303 if (written_bytes < 0 || remaining_bytes > 0) {
304 TFLITE_LOG_PROD(TFLITE_LOG_INFO,
305 "Failed to write Model to pipe: %s. Expect to write %d "
306 "bytes, %d bytes written.",
307 strerror(errno), remaining_bytes, written_bytes);
308 }
309 });
310 }
311
312 for (int i = 0; i < user_args.size(); i++) {
313 args_string.push_back(user_args[i]);
314 }
315 std::vector<std::vector<char>> args_char(args_string.size());
316 std::vector<char*> argv(args_string.size());
317 for (int i = 0; i < args_string.size(); i++) {
318 args_char[i] = {args_string[i].begin(), args_string[i].end()};
319 // Compiler adds '\0' for std::string to indicate the end of string
320 // automatically. For char* string, '\0' needs to be add at the end of
321 // string manually.
322 args_char[i].push_back('\0');
323 argv[i] = args_char[i].data();
324 }
325
326 int (*function_pointer)(int, char**) =
327 reinterpret_cast<int (*)(int, char**)>(function_pointer_);
328 int exit_code = __W_EXITCODE(function_pointer(argv.size(), argv.data()), 0);
329 if (write_thread.joinable()) {
330 write_thread.join();
331 }
332 return exit_code;
333 }
334 #endif // !__ANDROID__
335
336 namespace {
337
338 // kDontNeedShellEscapeChars and ShellEscape are copied from absl, which
339 // copied them from Python. Copied here because tflite core libraries should
340 // not depend on absl (both for size reasons and for possible version skew):
341
342 static const char kDontNeedShellEscapeChars[] =
343 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
344 "abcdefghijklmnopqrstuvwxyz"
345 "0123456789+-_.=/:,@";
346
ShellEscape(const std::string & src)347 std::string ShellEscape(const std::string& src) {
348 if (!src.empty() && // empty string needs quotes
349 src.find_first_not_of(kDontNeedShellEscapeChars) == std::string::npos) {
350 // only contains chars that don't need quotes; it's fine
351 return src;
352 } else if (src.find('\'') == std::string::npos) { // NOLINT
353 // no single quotes; just wrap it in single quotes
354 return "'" + src + "'";
355 } else {
356 // needs double quote escaping
357 std::string result = "\"";
358 for (const char c : src) {
359 switch (c) {
360 case '\\':
361 case '$':
362 case '"':
363 case '`':
364 result.push_back('\\');
365 }
366 result.push_back(c);
367 }
368 result.push_back('"');
369 return result;
370 }
371 }
372 } // namespace
373
374 } // namespace acceleration
375 } // namespace tflite
376