591 lines
21 KiB
C++
591 lines
21 KiB
C++
// Copyright (C) 2018-2023 Intel Corporation
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
/**
|
|
* @brief a header file with common samples functionality
|
|
* @file common.hpp
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <list>
|
|
#include <map>
|
|
#include <random>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
// clang-format off
|
|
#include "openvino/openvino.hpp"
|
|
#include "slog.hpp"
|
|
// clang-format on
|
|
|
|
// @brief performance counters sort
|
|
static constexpr char pcSort[] = "sort";
|
|
static constexpr char pcNoSort[] = "no_sort";
|
|
static constexpr char pcSimpleSort[] = "simple_sort";
|
|
|
|
#ifndef UNUSED
|
|
# if defined(_MSC_VER) && !defined(__clang__)
|
|
# define UNUSED
|
|
# else
|
|
# define UNUSED __attribute__((unused))
|
|
# endif
|
|
#endif
|
|
|
|
/**
|
|
* @brief Unicode string wrappers
|
|
*/
|
|
#if defined(ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32)
|
|
# define tchar wchar_t
|
|
# define tstring std::wstring
|
|
# define tmain wmain
|
|
# define TSTRING2STRING(tstr) wstring2string(tstr)
|
|
#else
|
|
# define tchar char
|
|
# define tstring std::string
|
|
# define tmain main
|
|
# define TSTRING2STRING(tstr) tstr
|
|
#endif
|
|
|
|
#if defined(ENABLE_UNICODE_PATH_SUPPORT) && defined(_WIN32)
|
|
|
|
/**
|
|
* @brief Convert wstring to string
|
|
* @param ref on wstring
|
|
* @return string
|
|
*/
|
|
inline std::string wstring2string(const std::wstring& wstr) {
|
|
std::string str;
|
|
for (auto&& wc : wstr)
|
|
str += static_cast<char>(wc);
|
|
return str;
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* @brief trim from start (in place)
|
|
* @param s - string to trim
|
|
*/
|
|
inline void ltrim(std::string& s) {
|
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int c) {
|
|
return !std::isspace(c);
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @brief trim from end (in place)
|
|
* @param s - string to trim
|
|
*/
|
|
inline void rtrim(std::string& s) {
|
|
s.erase(std::find_if(s.rbegin(),
|
|
s.rend(),
|
|
[](int c) {
|
|
return !std::isspace(c);
|
|
})
|
|
.base(),
|
|
s.end());
|
|
}
|
|
|
|
/**
|
|
* @brief trim from both ends (in place)
|
|
* @param s - string to trim
|
|
*/
|
|
inline std::string& trim(std::string& s) {
|
|
ltrim(s);
|
|
rtrim(s);
|
|
return s;
|
|
}
|
|
/**
|
|
* @brief Gets filename without extension
|
|
* @param filepath - full file name
|
|
* @return filename without extension
|
|
*/
|
|
inline std::string fileNameNoExt(const std::string& filepath) {
|
|
auto pos = filepath.rfind('.');
|
|
if (pos == std::string::npos)
|
|
return filepath;
|
|
return filepath.substr(0, pos);
|
|
}
|
|
|
|
/**
|
|
* @brief Get extension from filename
|
|
* @param filename - name of the file which extension should be extracted
|
|
* @return string with extracted file extension
|
|
*/
|
|
inline std::string fileExt(const std::string& filename) {
|
|
auto pos = filename.rfind('.');
|
|
if (pos == std::string::npos)
|
|
return "";
|
|
return filename.substr(pos + 1);
|
|
}
|
|
|
|
inline slog::LogStream& operator<<(slog::LogStream& os, const ov::Version& version) {
|
|
os << "Build ................................. ";
|
|
os << version.buildNumber << slog::endl;
|
|
|
|
return os;
|
|
}
|
|
|
|
inline slog::LogStream& operator<<(slog::LogStream& os, const std::map<std::string, ov::Version>& versions) {
|
|
for (auto&& version : versions) {
|
|
os << version.first << slog::endl;
|
|
os << version.second << slog::endl;
|
|
}
|
|
|
|
return os;
|
|
}
|
|
|
|
/**
|
|
* @brief Writes output data to BMP image
|
|
* @param name - image name
|
|
* @param data - output data
|
|
* @param height - height of the target image
|
|
* @param width - width of the target image
|
|
* @return false if error else true
|
|
*/
|
|
static UNUSED bool writeOutputBmp(std::string name, unsigned char* data, size_t height, size_t width) {
|
|
std::ofstream outFile;
|
|
outFile.open(name, std::ofstream::binary);
|
|
if (!outFile.is_open()) {
|
|
return false;
|
|
}
|
|
|
|
unsigned char file[14] = {
|
|
'B',
|
|
'M', // magic
|
|
0,
|
|
0,
|
|
0,
|
|
0, // size in bytes
|
|
0,
|
|
0, // app data
|
|
0,
|
|
0, // app data
|
|
40 + 14,
|
|
0,
|
|
0,
|
|
0 // start of data offset
|
|
};
|
|
unsigned char info[40] = {
|
|
40, 0, 0, 0, // info hd size
|
|
0, 0, 0, 0, // width
|
|
0, 0, 0, 0, // height
|
|
1, 0, // number color planes
|
|
24, 0, // bits per pixel
|
|
0, 0, 0, 0, // compression is none
|
|
0, 0, 0, 0, // image bits size
|
|
0x13, 0x0B, 0, 0, // horz resolution in pixel / m
|
|
0x13, 0x0B, 0, 0, // vert resolution (0x03C3 = 96 dpi, 0x0B13 = 72 dpi)
|
|
0, 0, 0, 0, // #colors in palette
|
|
0, 0, 0, 0, // #important colors
|
|
};
|
|
|
|
OPENVINO_ASSERT(
|
|
height < (size_t)std::numeric_limits<int32_t>::max && width < (size_t)std::numeric_limits<int32_t>::max,
|
|
"File size is too big: ",
|
|
height,
|
|
" X ",
|
|
width);
|
|
|
|
int padSize = static_cast<int>(4 - (width * 3) % 4) % 4;
|
|
int sizeData = static_cast<int>(width * height * 3 + height * padSize);
|
|
int sizeAll = sizeData + sizeof(file) + sizeof(info);
|
|
|
|
file[2] = (unsigned char)(sizeAll);
|
|
file[3] = (unsigned char)(sizeAll >> 8);
|
|
file[4] = (unsigned char)(sizeAll >> 16);
|
|
file[5] = (unsigned char)(sizeAll >> 24);
|
|
|
|
info[4] = (unsigned char)(width);
|
|
info[5] = (unsigned char)(width >> 8);
|
|
info[6] = (unsigned char)(width >> 16);
|
|
info[7] = (unsigned char)(width >> 24);
|
|
|
|
int32_t negativeHeight = -(int32_t)height;
|
|
info[8] = (unsigned char)(negativeHeight);
|
|
info[9] = (unsigned char)(negativeHeight >> 8);
|
|
info[10] = (unsigned char)(negativeHeight >> 16);
|
|
info[11] = (unsigned char)(negativeHeight >> 24);
|
|
|
|
info[20] = (unsigned char)(sizeData);
|
|
info[21] = (unsigned char)(sizeData >> 8);
|
|
info[22] = (unsigned char)(sizeData >> 16);
|
|
info[23] = (unsigned char)(sizeData >> 24);
|
|
|
|
outFile.write(reinterpret_cast<char*>(file), sizeof(file));
|
|
outFile.write(reinterpret_cast<char*>(info), sizeof(info));
|
|
|
|
unsigned char pad[3] = {0, 0, 0};
|
|
|
|
for (size_t y = 0; y < height; y++) {
|
|
for (size_t x = 0; x < width; x++) {
|
|
unsigned char pixel[3];
|
|
pixel[0] = data[y * width * 3 + x * 3];
|
|
pixel[1] = data[y * width * 3 + x * 3 + 1];
|
|
pixel[2] = data[y * width * 3 + x * 3 + 2];
|
|
|
|
outFile.write(reinterpret_cast<char*>(pixel), 3);
|
|
}
|
|
outFile.write(reinterpret_cast<char*>(pad), padSize);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Adds colored rectangles to the image
|
|
* @param data - data where rectangles are put
|
|
* @param height - height of the rectangle
|
|
* @param width - width of the rectangle
|
|
* @param rectangles - vector points for the rectangle, should be 4x compared to num classes
|
|
* @param classes - vector of classes
|
|
* @param thickness - thickness of a line (in pixels) to be used for bounding boxes
|
|
*/
|
|
static UNUSED void addRectangles(unsigned char* data,
|
|
size_t height,
|
|
size_t width,
|
|
std::vector<int> rectangles,
|
|
std::vector<int> classes,
|
|
int thickness) {
|
|
struct Color {
|
|
unsigned char red;
|
|
unsigned char green;
|
|
unsigned char blue;
|
|
};
|
|
std::vector<Color> colors = {// colors to be used for bounding boxes
|
|
{128, 64, 128}, {232, 35, 244}, {70, 70, 70}, {156, 102, 102}, {153, 153, 190},
|
|
{153, 153, 153}, {30, 170, 250}, {0, 220, 220}, {35, 142, 107}, {152, 251, 152},
|
|
{180, 130, 70}, {60, 20, 220}, {0, 0, 255}, {142, 0, 0}, {70, 0, 0},
|
|
{100, 60, 0}, {90, 0, 0}, {230, 0, 0}, {32, 11, 119}, {0, 74, 111},
|
|
{81, 0, 81}};
|
|
|
|
if (rectangles.size() % 4 != 0 || rectangles.size() / 4 != classes.size()) {
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < classes.size(); i++) {
|
|
int x = rectangles.at(i * 4);
|
|
int y = rectangles.at(i * 4 + 1);
|
|
int w = rectangles.at(i * 4 + 2);
|
|
int h = rectangles.at(i * 4 + 3);
|
|
|
|
int cls = classes.at(i) % colors.size(); // color of a bounding box line
|
|
|
|
if (x < 0)
|
|
x = 0;
|
|
if (y < 0)
|
|
y = 0;
|
|
if (w < 0)
|
|
w = 0;
|
|
if (h < 0)
|
|
h = 0;
|
|
|
|
if (static_cast<std::size_t>(x) >= width) {
|
|
x = static_cast<int>(width - 1);
|
|
w = 0;
|
|
thickness = 1;
|
|
}
|
|
if (static_cast<std::size_t>(y) >= height) {
|
|
y = static_cast<int>(height - 1);
|
|
h = 0;
|
|
thickness = 1;
|
|
}
|
|
|
|
if ((static_cast<std::size_t>(x) + w) >= width) {
|
|
w = static_cast<int>(width - x - 1);
|
|
}
|
|
if ((static_cast<std::size_t>(y) + h) >= height) {
|
|
h = static_cast<int>(height - y - 1);
|
|
}
|
|
|
|
thickness = std::min(std::min(thickness, w / 2 + 1), h / 2 + 1);
|
|
|
|
size_t shift_first;
|
|
size_t shift_second;
|
|
for (int t = 0; t < thickness; t++) {
|
|
shift_first = (y + t) * width * 3;
|
|
shift_second = (y + h - t) * width * 3;
|
|
for (int ii = x; ii < x + w + 1; ii++) {
|
|
data[shift_first + ii * 3] = colors.at(cls).red;
|
|
data[shift_first + ii * 3 + 1] = colors.at(cls).green;
|
|
data[shift_first + ii * 3 + 2] = colors.at(cls).blue;
|
|
data[shift_second + ii * 3] = colors.at(cls).red;
|
|
data[shift_second + ii * 3 + 1] = colors.at(cls).green;
|
|
data[shift_second + ii * 3 + 2] = colors.at(cls).blue;
|
|
}
|
|
}
|
|
|
|
for (int t = 0; t < thickness; t++) {
|
|
shift_first = (x + t) * 3;
|
|
shift_second = (x + w - t) * 3;
|
|
for (int ii = y; ii < y + h + 1; ii++) {
|
|
data[shift_first + ii * width * 3] = colors.at(cls).red;
|
|
data[shift_first + ii * width * 3 + 1] = colors.at(cls).green;
|
|
data[shift_first + ii * width * 3 + 2] = colors.at(cls).blue;
|
|
data[shift_second + ii * width * 3] = colors.at(cls).red;
|
|
data[shift_second + ii * width * 3 + 1] = colors.at(cls).green;
|
|
data[shift_second + ii * width * 3 + 2] = colors.at(cls).blue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void showAvailableDevices() {
|
|
ov::Core core;
|
|
std::vector<std::string> devices = core.get_available_devices();
|
|
|
|
std::cout << std::endl;
|
|
std::cout << "Available target devices:";
|
|
for (const auto& device : devices) {
|
|
std::cout << " " << device;
|
|
}
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
inline std::string getFullDeviceName(ov::Core& core, std::string device) {
|
|
try {
|
|
return core.get_property(device, ov::device::full_name);
|
|
} catch (ov::Exception&) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
static UNUSED void printPerformanceCounts(std::vector<ov::ProfilingInfo> performanceData,
|
|
std::ostream& stream,
|
|
std::string deviceName,
|
|
bool bshowHeader = true,
|
|
int precision = 3) {
|
|
std::chrono::microseconds totalTime = std::chrono::microseconds::zero();
|
|
std::chrono::microseconds totalTimeCpu = std::chrono::microseconds::zero();
|
|
// Print performance counts
|
|
if (bshowHeader) {
|
|
stream << std::endl << "Performance counts:" << std::endl << std::endl;
|
|
}
|
|
std::ios::fmtflags fmt(std::cout.flags());
|
|
stream << std::fixed << std::setprecision(precision);
|
|
|
|
for (const auto& it : performanceData) {
|
|
if (it.real_time.count() > 0) {
|
|
totalTime += it.real_time;
|
|
}
|
|
if (it.cpu_time.count() > 0) {
|
|
totalTimeCpu += it.cpu_time;
|
|
}
|
|
|
|
std::string toPrint(it.node_name);
|
|
const int maxPrintLength = 20;
|
|
|
|
if (it.node_name.length() >= maxPrintLength) {
|
|
toPrint = it.node_name.substr(0, maxPrintLength - 5);
|
|
toPrint += "...";
|
|
}
|
|
|
|
stream << std::setw(maxPrintLength) << std::left << toPrint << " ";
|
|
switch (it.status) {
|
|
case ov::ProfilingInfo::Status::EXECUTED:
|
|
stream << std::setw(21) << std::left << "EXECUTED ";
|
|
break;
|
|
case ov::ProfilingInfo::Status::NOT_RUN:
|
|
stream << std::setw(21) << std::left << "NOT_RUN ";
|
|
break;
|
|
case ov::ProfilingInfo::Status::OPTIMIZED_OUT:
|
|
stream << std::setw(21) << std::left << "OPTIMIZED_OUT ";
|
|
break;
|
|
}
|
|
|
|
stream << "layerType: ";
|
|
if (it.node_type.length() >= maxPrintLength) {
|
|
stream << std::setw(maxPrintLength) << std::left << it.node_type.substr(0, maxPrintLength - 3) + "..."
|
|
<< " ";
|
|
} else {
|
|
stream << std::setw(maxPrintLength) << std::left << it.node_type << " ";
|
|
}
|
|
|
|
stream << std::setw(30) << std::left << "execType: " + std::string(it.exec_type) << " ";
|
|
stream << "realTime (ms): " << std::setw(10) << std::left << std::fixed << std::setprecision(3)
|
|
<< it.real_time.count() / 1000.0 << " ";
|
|
stream << "cpuTime (ms): " << std::setw(10) << std::left << std::fixed << std::setprecision(3)
|
|
<< it.cpu_time.count() / 1000.0 << " ";
|
|
stream << std::endl;
|
|
}
|
|
stream << std::setw(25) << std::left << "Total time: " << std::fixed << std::setprecision(3)
|
|
<< totalTime.count() / 1000.0 << " milliseconds" << std::endl;
|
|
stream << std::setw(25) << std::left << "Total CPU time: " << std::fixed << std::setprecision(3)
|
|
<< totalTimeCpu.count() / 1000.0 << " milliseconds" << std::endl;
|
|
stream << std::endl;
|
|
stream << "Full device name: " << deviceName << std::endl;
|
|
stream << std::endl;
|
|
stream.flags(fmt);
|
|
}
|
|
|
|
static inline std::string double_to_string(const double number) {
|
|
std::stringstream ss;
|
|
ss << std::fixed << std::setprecision(2) << number;
|
|
return ss.str();
|
|
}
|
|
|
|
template <typename T>
|
|
using uniformDistribution = typename std::conditional<
|
|
std::is_floating_point<T>::value,
|
|
std::uniform_real_distribution<T>,
|
|
typename std::conditional<std::is_integral<T>::value, std::uniform_int_distribution<T>, void>::type>::type;
|
|
|
|
template <typename T, typename T2>
|
|
static inline void fill_random(ov::Tensor& tensor,
|
|
T rand_min = std::numeric_limits<uint8_t>::min(),
|
|
T rand_max = std::numeric_limits<uint8_t>::max()) {
|
|
std::mt19937 gen(0);
|
|
size_t tensor_size = tensor.get_size();
|
|
if (0 == tensor_size) {
|
|
throw std::runtime_error(
|
|
"Models with dynamic shapes aren't supported. Input tensors must have specific shapes before inference");
|
|
}
|
|
T* data = tensor.data<T>();
|
|
uniformDistribution<T2> distribution(rand_min, rand_max);
|
|
for (size_t i = 0; i < tensor_size; i++) {
|
|
data[i] = static_cast<T>(distribution(gen));
|
|
}
|
|
}
|
|
|
|
static inline void fill_tensor_random(ov::Tensor tensor) {
|
|
switch (tensor.get_element_type()) {
|
|
case ov::element::f32:
|
|
fill_random<float, float>(tensor);
|
|
break;
|
|
case ov::element::f64:
|
|
fill_random<double, double>(tensor);
|
|
break;
|
|
case ov::element::f16:
|
|
fill_random<short, short>(tensor);
|
|
break;
|
|
case ov::element::i32:
|
|
fill_random<int32_t, int32_t>(tensor);
|
|
break;
|
|
case ov::element::i64:
|
|
fill_random<int64_t, int64_t>(tensor);
|
|
break;
|
|
case ov::element::u8:
|
|
// uniform_int_distribution<uint8_t> is not allowed in the C++17
|
|
// standard and vs2017/19
|
|
fill_random<uint8_t, uint32_t>(tensor);
|
|
break;
|
|
case ov::element::i8:
|
|
// uniform_int_distribution<int8_t> is not allowed in the C++17 standard
|
|
// and vs2017/19
|
|
fill_random<int8_t, int32_t>(tensor, std::numeric_limits<int8_t>::min(), std::numeric_limits<int8_t>::max());
|
|
break;
|
|
case ov::element::u16:
|
|
fill_random<uint16_t, uint16_t>(tensor);
|
|
break;
|
|
case ov::element::i16:
|
|
fill_random<int16_t, int16_t>(tensor);
|
|
break;
|
|
case ov::element::boolean:
|
|
fill_random<uint8_t, uint32_t>(tensor, 0, 1);
|
|
break;
|
|
default:
|
|
OPENVINO_THROW("Input type is not supported for a tensor");
|
|
}
|
|
}
|
|
|
|
static UNUSED bool sort_pc_descend(const ov::ProfilingInfo& profiling1, const ov::ProfilingInfo& profiling2) {
|
|
return profiling1.real_time > profiling2.real_time;
|
|
}
|
|
|
|
static UNUSED void printPerformanceCountsSort(std::vector<ov::ProfilingInfo> performanceData,
|
|
std::ostream& stream,
|
|
std::string deviceName,
|
|
std::string sorttype,
|
|
bool bshowHeader = true,
|
|
int precision = 3) {
|
|
std::chrono::microseconds totalTime = std::chrono::microseconds::zero();
|
|
std::chrono::microseconds totalTimeCpu = std::chrono::microseconds::zero();
|
|
|
|
// Print performance counts
|
|
if (bshowHeader) {
|
|
stream << std::endl << "Performance counts:" << std::endl << std::endl;
|
|
}
|
|
std::ios::fmtflags fmt(std::cout.flags());
|
|
stream << std::fixed << std::setprecision(precision);
|
|
|
|
for (const auto& it : performanceData) {
|
|
if (it.real_time.count() > 0) {
|
|
totalTime += it.real_time;
|
|
}
|
|
if (it.cpu_time.count() > 0) {
|
|
totalTimeCpu += it.cpu_time;
|
|
}
|
|
}
|
|
if (totalTime.count() != 0) {
|
|
std::vector<ov::ProfilingInfo> sortPerfCounts{std::begin(performanceData), std::end(performanceData)};
|
|
if (sorttype == pcSort || sorttype == pcSimpleSort) {
|
|
std::sort(sortPerfCounts.begin(), sortPerfCounts.end(), sort_pc_descend);
|
|
}
|
|
|
|
for (const auto& it : sortPerfCounts) {
|
|
if ((sorttype == pcSimpleSort && it.status == ov::ProfilingInfo::Status::EXECUTED) ||
|
|
sorttype != pcSimpleSort) {
|
|
std::string toPrint(it.node_name);
|
|
const int maxPrintLength = 20;
|
|
|
|
if (it.node_name.length() >= maxPrintLength) {
|
|
toPrint = it.node_name.substr(0, maxPrintLength - 5);
|
|
toPrint += "...";
|
|
}
|
|
|
|
stream << std::setw(maxPrintLength) << std::left << toPrint << " ";
|
|
switch (it.status) {
|
|
case ov::ProfilingInfo::Status::EXECUTED:
|
|
stream << std::setw(21) << std::left << "EXECUTED ";
|
|
break;
|
|
case ov::ProfilingInfo::Status::NOT_RUN:
|
|
stream << std::setw(21) << std::left << "NOT_RUN ";
|
|
break;
|
|
case ov::ProfilingInfo::Status::OPTIMIZED_OUT:
|
|
stream << std::setw(21) << std::left << "OPTIMIZED_OUT ";
|
|
break;
|
|
}
|
|
|
|
stream << "layerType: ";
|
|
if (it.node_type.length() >= maxPrintLength) {
|
|
stream << std::setw(maxPrintLength) << std::left
|
|
<< it.node_type.substr(0, maxPrintLength - 3) + "..."
|
|
<< " ";
|
|
} else {
|
|
stream << std::setw(maxPrintLength) << std::left << it.node_type << " ";
|
|
}
|
|
|
|
stream << std::setw(30) << std::left << "execType: " + std::string(it.exec_type) << " ";
|
|
stream << "realTime (ms): " << std::setw(10) << std::left << std::fixed << std::setprecision(3)
|
|
<< it.real_time.count() / 1000.0 << " ";
|
|
stream << "cpuTime (ms): " << std::setw(10) << std::left << std::fixed << std::setprecision(3)
|
|
<< it.cpu_time.count() / 1000.0 << " ";
|
|
|
|
double opt_proportion = it.real_time.count() * 100.0 / totalTime.count();
|
|
std::stringstream opt_proportion_ss;
|
|
opt_proportion_ss << std::fixed << std::setprecision(2) << opt_proportion;
|
|
std::string opt_proportion_str = opt_proportion_ss.str();
|
|
if (opt_proportion_str == "0.00") {
|
|
opt_proportion_str = "N/A";
|
|
}
|
|
stream << std::setw(20) << std::left << "proportion: " + opt_proportion_str + "%";
|
|
stream << std::endl;
|
|
}
|
|
}
|
|
}
|
|
stream << std::setw(25) << std::left << "Total time: " + std::to_string(totalTime.count() / 1000.0)
|
|
<< " milliseconds" << std::endl;
|
|
stream << std::endl;
|
|
stream << "Full device name: " << deviceName << std::endl;
|
|
stream << std::endl;
|
|
stream.flags(fmt);
|
|
}
|