xref: /aosp_15_r20/external/tensorflow/tensorflow/lite/experimental/acceleration/mini_benchmark/runner.cc (revision b6fb3261f9314811a0f4371741dbb8839866f948)
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