1 /*
2  * Copyright (c) 2023 Huawei Device Co., Ltd.
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 
16 #include <fstream>
17 #include <sstream>
18 #include "hitrace_meter.h"
19 #include "hcodec.h"
20 #include "hcodec_log.h"
21 #include "hcodec_dfx.h"
22 #include "hcodec_utils.h"
23 
24 namespace OHOS::MediaAVCodec {
25 using namespace std;
26 
ScopedTrace(const std::string &value)27 ScopedTrace::ScopedTrace(const std::string &value)
28 {
29     StartTrace(HITRACE_TAG_ZMEDIA, value);
30 }
31 
~ScopedTrace()32 ScopedTrace::~ScopedTrace()
33 {
34     FinishTrace(HITRACE_TAG_ZMEDIA);
35 }
36 
FuncTracker(std::string value)37 FuncTracker::FuncTracker(std::string value) : value_(std::move(value))
38 {
39     PLOGI("%s >>", value_.c_str());
40 }
41 
~FuncTracker()42 FuncTracker::~FuncTracker()
43 {
44     PLOGI("%s <<", value_.c_str());
45 }
46 
OnPrintAllBufferOwner(const MsgInfo& msg)47 void HCodec::OnPrintAllBufferOwner(const MsgInfo& msg)
48 {
49     std::chrono::time_point<std::chrono::steady_clock> lastOwnerChangeTime;
50     msg.param->GetValue(KEY_LAST_OWNER_CHANGE_TIME, lastOwnerChangeTime);
51     if ((lastOwnerChangeTime == lastOwnerChangeTime_) &&
52         (circulateWarnPrintedTimes_ < MAX_CIRCULATE_WARN_TIMES)) {
53         HLOGW("buffer circulate stoped");
54         UpdateOwner();
55         PrintAllBufferInfo();
56         circulateWarnPrintedTimes_++;
57     }
58     ParamSP param = make_shared<ParamBundle>();
59     param->SetValue(KEY_LAST_OWNER_CHANGE_TIME, lastOwnerChangeTime_);
60     SendAsyncMsg(MsgWhat::PRINT_ALL_BUFFER_OWNER, param,
61                  THREE_SECONDS_IN_US * (circulateWarnPrintedTimes_ + 1));
62 }
63 
PrintAllBufferInfo()64 void HCodec::PrintAllBufferInfo()
65 {
66     auto now = chrono::steady_clock::now();
67     PrintAllBufferInfo(true, now);
68     PrintAllBufferInfo(false, now);
69 }
70 
PrintAllBufferInfo(bool isInput, std::chrono::time_point<std::chrono::steady_clock> now)71 void HCodec::PrintAllBufferInfo(bool isInput, std::chrono::time_point<std::chrono::steady_clock> now)
72 {
73     const char* inOutStr = isInput ? " in" : "out";
74     bool eos = isInput ? inputPortEos_ : outputPortEos_;
75     uint64_t cnt = isInput ? inTotalCnt_ : outRecord_.totalCnt;
76     const std::array<int, OWNER_CNT>& arr = isInput ? inputOwner_ : outputOwner_;
77     const vector<BufferInfo>& pool = isInput ? inputBufferPool_ : outputBufferPool_;
78 
79     std::stringstream s;
80     for (const BufferInfo& info : pool) {
81         int64_t holdMs = chrono::duration_cast<chrono::milliseconds>(now - info.lastOwnerChangeTime).count();
82         s << info.bufferId << ":" << ToString(info.owner) << "(" << holdMs << "), ";
83     }
84     HLOGI("%s: eos=%d, cnt=%" PRIu64 ", %d/%d/%d/%d, %s", inOutStr, eos, cnt,
85           arr[OWNED_BY_US], arr[OWNED_BY_USER], arr[OWNED_BY_OMX], arr[OWNED_BY_SURFACE], s.str().c_str());
86 }
87 
OnGetHidumperInfo()88 std::string HCodec::OnGetHidumperInfo()
89 {
90     auto now = chrono::steady_clock::now();
91     std::stringstream s;
92     auto getbufferCapacity = [](const std::vector<BufferInfo> &pool) -> int32_t {
93         IF_TRUE_RETURN_VAL(pool.empty(), 0);
94         IF_TRUE_RETURN_VAL(pool.front().surfaceBuffer, pool.front().surfaceBuffer->GetSize());
95         IF_TRUE_RETURN_VAL(!(pool.front().avBuffer && pool.front().avBuffer->memory_), 0);
96         return pool.front().avBuffer->memory_->GetCapacity();
97     };
98 
99     s << endl;
100     s << "        " << compUniqueStr_ << "[" << currState_->GetName() << "]" << endl;
101     s << "        " << "------------INPUT-----------" << endl;
102     s << "        " << "eos:" << inputPortEos_ << ", etb:" << inTotalCnt_
103       << ", bufferCapacity:" << getbufferCapacity(inputBufferPool_) << endl;
104     for (const BufferInfo& info : inputBufferPool_) {
105         int64_t holdMs = chrono::duration_cast<chrono::milliseconds>(now - info.lastOwnerChangeTime).count();
106         s << "        " << "inBufId = " << info.bufferId << ", owner = " << ToString(info.owner)
107           << ", holdMs = " << holdMs << endl;
108     }
109     s << "        " << "----------------------------" << endl;
110     s << "        " << "------------OUTPUT----------" << endl;
111     s << "        " << "eos:" << outputPortEos_ << ", fbd:" << outRecord_.totalCnt
112       << ", bufferCapacity:" << getbufferCapacity(outputBufferPool_) << endl;
113     for (const BufferInfo& info : outputBufferPool_) {
114         int64_t holdMs = chrono::duration_cast<chrono::milliseconds>(now - info.lastOwnerChangeTime).count();
115         s << "        " << "outBufId = " << info.bufferId << ", owner = " << ToString(info.owner)
116           << ", holdMs = " << holdMs << endl;
117     }
118     s << "        " << "----------------------------" << endl;
119     return s.str();
120 }
121 
UpdateOwner()122 void HCodec::UpdateOwner()
123 {
124     UpdateOwner(true);
125     UpdateOwner(false);
126 }
127 
UpdateOwner(bool isInput)128 void HCodec::UpdateOwner(bool isInput)
129 {
130     std::array<int, OWNER_CNT>& arr = isInput ? inputOwner_ : outputOwner_;
131     const std::array<std::string, OWNER_CNT>& ownerStr = isInput ? inputOwnerStr_ : outputOwnerStr_;
132     const vector<BufferInfo>& pool = isInput ? inputBufferPool_ : outputBufferPool_;
133 
134     arr.fill(0);
135     for (const BufferInfo &info : pool) {
136         arr[info.owner]++;
137     }
138     for (uint32_t owner = 0; owner < static_cast<uint32_t>(OWNER_CNT); owner++) {
139         CountTrace(HITRACE_TAG_ZMEDIA, ownerStr[owner], arr[owner]);
140     }
141 }
142 
ReduceOwner(bool isInput, BufferOwner owner)143 void HCodec::ReduceOwner(bool isInput, BufferOwner owner)
144 {
145     std::array<int, OWNER_CNT>& arr = isInput ? inputOwner_ : outputOwner_;
146     const std::array<std::string, OWNER_CNT>& ownerStr = isInput ? inputOwnerStr_ : outputOwnerStr_;
147     arr[owner]--;
148     CountTrace(HITRACE_TAG_ZMEDIA, ownerStr[owner], arr[owner]);
149 }
150 
PrintStatistic(bool isInput, std::chrono::time_point<std::chrono::steady_clock> now)151 void HCodec::PrintStatistic(bool isInput, std::chrono::time_point<std::chrono::steady_clock> now)
152 {
153     int64_t fromFirstToNow = chrono::duration_cast<chrono::microseconds>(
154         now - (isInput ? firstInTime_ : firstOutTime_)).count();
155     if (fromFirstToNow == 0) {
156         return;
157     }
158     double fps = PRINT_PER_FRAME * US_TO_S / fromFirstToNow;
159     const std::array<int, OWNER_CNT>& arr = isInput ? inputOwner_ : outputOwner_;
160     double aveHoldMs[OWNER_CNT];
161     for (uint32_t owner = 0; owner < static_cast<uint32_t>(OWNER_CNT); owner++) {
162         TotalCntAndCost& holdRecord = isInput ? inputHoldTimeRecord_[owner] : outputHoldTimeRecord_[owner];
163         aveHoldMs[owner] = (holdRecord.totalCnt == 0) ? -1 : (holdRecord.totalCostUs / US_TO_MS / holdRecord.totalCnt);
164     }
165     HLOGI("%s:%.0f; %d/%d/%d/%d, %.0f/%.0f/%.0f/%.0f", (isInput ? " in" : "out"), fps,
166         arr[OWNED_BY_US], arr[OWNED_BY_USER], arr[OWNED_BY_OMX], arr[OWNED_BY_SURFACE],
167         aveHoldMs[OWNED_BY_US], aveHoldMs[OWNED_BY_USER], aveHoldMs[OWNED_BY_OMX], aveHoldMs[OWNED_BY_SURFACE]);
168 }
169 
ChangeOwner(BufferInfo& info, BufferOwner newOwner)170 void HCodec::ChangeOwner(BufferInfo& info, BufferOwner newOwner)
171 {
172     debugMode_ ? ChangeOwnerDebug(info, newOwner) : ChangeOwnerNormal(info, newOwner);
173 }
174 
ChangeOwnerNormal(BufferInfo& info, BufferOwner newOwner)175 void HCodec::ChangeOwnerNormal(BufferInfo& info, BufferOwner newOwner)
176 {
177     std::array<int, OWNER_CNT>& arr = info.isInput ? inputOwner_ : outputOwner_;
178     const std::array<std::string, OWNER_CNT>& ownerStr = info.isInput ? inputOwnerStr_ : outputOwnerStr_;
179 
180     BufferOwner oldOwner = info.owner;
181     auto now = chrono::steady_clock::now();
182     lastOwnerChangeTime_ = now;
183     circulateWarnPrintedTimes_ = 0;
184     arr[oldOwner]--;
185     arr[newOwner]++;
186     CountTrace(HITRACE_TAG_ZMEDIA, ownerStr[oldOwner], arr[oldOwner]);
187     CountTrace(HITRACE_TAG_ZMEDIA, ownerStr[newOwner], arr[newOwner]);
188 
189     uint64_t holdUs = static_cast<uint64_t>(
190         chrono::duration_cast<chrono::microseconds>(now - info.lastOwnerChangeTime).count());
191     TotalCntAndCost& holdRecord = info.isInput ? inputHoldTimeRecord_[oldOwner] :
192                                                 outputHoldTimeRecord_[oldOwner];
193     holdRecord.totalCnt++;
194     holdRecord.totalCostUs += holdUs;
195 
196     // now change owner
197     info.lastOwnerChangeTime = now;
198     info.owner = newOwner;
199 
200     if (info.isInput && oldOwner == OWNED_BY_US && newOwner == OWNED_BY_OMX) {
201         if (inTotalCnt_ == 0) {
202             firstInTime_ = now;
203         }
204         inTotalCnt_++;
205         if (inTotalCnt_ % PRINT_PER_FRAME == 0) {
206             PrintStatistic(info.isInput, now);
207             firstInTime_ = now;
208             inputHoldTimeRecord_.fill({0, 0});
209         }
210     }
211     if (!info.isInput && oldOwner == OWNED_BY_OMX && newOwner == OWNED_BY_US) {
212         if (outRecord_.totalCnt == 0) {
213             firstOutTime_ = now;
214         }
215         outRecord_.totalCnt++;
216         if (outRecord_.totalCnt % PRINT_PER_FRAME == 0) {
217             PrintStatistic(info.isInput, now);
218             firstOutTime_ = now;
219             outputHoldTimeRecord_.fill({0, 0});
220         }
221     }
222 }
223 
ChangeOwnerDebug(BufferInfo& info, BufferOwner newOwner)224 void HCodec::ChangeOwnerDebug(BufferInfo& info, BufferOwner newOwner)
225 {
226     std::array<int, OWNER_CNT>& arr = info.isInput ? inputOwner_ : outputOwner_;
227     const std::array<std::string, OWNER_CNT>& ownerStr = info.isInput ? inputOwnerStr_ : outputOwnerStr_;
228 
229     BufferOwner oldOwner = info.owner;
230     auto now = chrono::steady_clock::now();
231     lastOwnerChangeTime_ = now;
232     circulateWarnPrintedTimes_ = 0;
233     arr[oldOwner]--;
234     arr[newOwner]++;
235     CountTrace(HITRACE_TAG_ZMEDIA, ownerStr[oldOwner], arr[oldOwner]);
236     CountTrace(HITRACE_TAG_ZMEDIA, ownerStr[newOwner], arr[newOwner]);
237 
238     const char* oldOwnerStr = ToString(oldOwner);
239     const char* newOwnerStr = ToString(newOwner);
240     const char* idStr = info.isInput ? "inBufId" : "outBufId";
241 
242     uint64_t holdUs = static_cast<uint64_t>
243         (chrono::duration_cast<chrono::microseconds>(now - info.lastOwnerChangeTime).count());
244     double holdMs = holdUs / US_TO_MS;
245     TotalCntAndCost& holdRecord = info.isInput ? inputHoldTimeRecord_[oldOwner] :
246                                                 outputHoldTimeRecord_[oldOwner];
247     holdRecord.totalCnt++;
248     holdRecord.totalCostUs += holdUs;
249     double aveHoldMs = holdRecord.totalCostUs / US_TO_MS / holdRecord.totalCnt;
250 
251     // now change owner
252     info.lastOwnerChangeTime = now;
253     info.owner = newOwner;
254     HLOGI("%s = %u, after hold %.1f ms (%.1f ms), %s -> %s, %d/%d/%d/%d",
255           idStr, info.bufferId, holdMs, aveHoldMs, oldOwnerStr, newOwnerStr,
256           arr[OWNED_BY_US], arr[OWNED_BY_USER], arr[OWNED_BY_OMX], arr[OWNED_BY_SURFACE]);
257 
258     if (info.isInput && oldOwner == OWNED_BY_US && newOwner == OWNED_BY_OMX) {
259         UpdateInputRecord(info, now);
260     }
261     if (!info.isInput && oldOwner == OWNED_BY_OMX && newOwner == OWNED_BY_US) {
262         UpdateOutputRecord(info, now);
263     }
264 }
265 
UpdateInputRecord(const BufferInfo& info, std::chrono::time_point<std::chrono::steady_clock> now)266 void HCodec::UpdateInputRecord(const BufferInfo& info, std::chrono::time_point<std::chrono::steady_clock> now)
267 {
268     if (!info.IsValidFrame()) {
269         return;
270     }
271     inTimeMap_[info.omxBuffer->pts] = now;
272     if (inTotalCnt_ == 0) {
273         firstInTime_ = now;
274     }
275     inTotalCnt_++;
276 
277     uint64_t fromFirstInToNow = static_cast<uint64_t>
278         (chrono::duration_cast<chrono::microseconds>(now - firstInTime_).count());
279     if (fromFirstInToNow == 0) {
280         HLOGI("pts = %" PRId64 ", len = %u, flags = 0x%x",
281               info.omxBuffer->pts, info.omxBuffer->filledLen, info.omxBuffer->flag);
282     } else {
283         double inFps = inTotalCnt_ * US_TO_S / fromFirstInToNow;
284         HLOGI("pts = %" PRId64 ", len = %u, flags = 0x%x, in fps %.2f",
285               info.omxBuffer->pts, info.omxBuffer->filledLen, info.omxBuffer->flag, inFps);
286     }
287 }
288 
UpdateOutputRecord(const BufferInfo& info, std::chrono::time_point<std::chrono::steady_clock> now)289 void HCodec::UpdateOutputRecord(const BufferInfo& info, std::chrono::time_point<std::chrono::steady_clock> now)
290 {
291     if (!info.IsValidFrame()) {
292         return;
293     }
294     auto it = inTimeMap_.find(info.omxBuffer->pts);
295     if (it == inTimeMap_.end()) {
296         return;
297     }
298     if (outRecord_.totalCnt == 0) {
299         firstOutTime_ = now;
300     }
301     outRecord_.totalCnt++;
302 
303     uint64_t fromInToOut = static_cast<uint64_t>
304         (chrono::duration_cast<chrono::microseconds>(now - it->second).count());
305     inTimeMap_.erase(it);
306     outRecord_.totalCostUs += fromInToOut;
307     double oneFrameCostMs = fromInToOut / US_TO_MS;
308     double averageCostMs = outRecord_.totalCostUs / US_TO_MS / outRecord_.totalCnt;
309 
310     uint64_t fromFirstOutToNow = static_cast<uint64_t>
311         (chrono::duration_cast<chrono::microseconds>(now - firstOutTime_).count());
312     if (fromFirstOutToNow == 0) {
313         HLOGI("pts = %" PRId64 ", len = %u, flags = 0x%x, "
314               "cost %.2f ms (%.2f ms)",
315               info.omxBuffer->pts, info.omxBuffer->filledLen, info.omxBuffer->flag,
316               oneFrameCostMs, averageCostMs);
317     } else {
318         double outFps = outRecord_.totalCnt * US_TO_S / fromFirstOutToNow;
319         HLOGI("pts = %" PRId64 ", len = %u, flags = 0x%x, "
320               "cost %.2f ms (%.2f ms), out fps %.2f",
321               info.omxBuffer->pts, info.omxBuffer->filledLen, info.omxBuffer->flag,
322               oneFrameCostMs, averageCostMs, outFps);
323     }
324 }
325 
IsValidFrame() const326 bool HCodec::BufferInfo::IsValidFrame() const
327 {
328     if (omxBuffer->flag & OMX_BUFFERFLAG_EOS) {
329         return false;
330     }
331     if (omxBuffer->flag & OMX_BUFFERFLAG_CODECCONFIG) {
332         return false;
333     }
334     if (omxBuffer->filledLen == 0) {
335         return false;
336     }
337     return true;
338 }
339 
340 #ifdef BUILD_ENG_VERSION
Dump(const string& prefix, uint64_t cnt, DumpMode dumpMode, bool isEncoder) const341 void HCodec::BufferInfo::Dump(const string& prefix, uint64_t cnt, DumpMode dumpMode, bool isEncoder) const
342 {
343     if (isInput) {
344         if (((dumpMode & DUMP_ENCODER_INPUT) && isEncoder) ||
345             ((dumpMode & DUMP_DECODER_INPUT) && !isEncoder)) {
346             Dump(prefix + "_Input", cnt);
347         }
348     } else {
349         if (((dumpMode & DUMP_ENCODER_OUTPUT) && isEncoder) ||
350             ((dumpMode & DUMP_DECODER_OUTPUT) && !isEncoder)) {
351             Dump(prefix + "_Output", cnt);
352         }
353     }
354 }
355 
Dump(const string& prefix, uint64_t cnt) const356 void HCodec::BufferInfo::Dump(const string& prefix, uint64_t cnt) const
357 {
358     if (surfaceBuffer) {
359         DumpSurfaceBuffer(prefix, cnt);
360     } else {
361         DumpLinearBuffer(prefix);
362     }
363 }
364 
DumpSurfaceBuffer(const std::string& prefix, uint64_t cnt) const365 void HCodec::BufferInfo::DumpSurfaceBuffer(const std::string& prefix, uint64_t cnt) const
366 {
367     if (omxBuffer->filledLen == 0) {
368         return;
369     }
370     const char* va = reinterpret_cast<const char*>(surfaceBuffer->GetVirAddr());
371     IF_TRUE_RETURN_VOID_WITH_MSG(va == nullptr, "null va");
372     int w = surfaceBuffer->GetWidth();
373     int h = surfaceBuffer->GetHeight();
374     int byteStride = surfaceBuffer->GetStride();
375     IF_TRUE_RETURN_VOID_WITH_MSG(byteStride == 0, "stride 0");
376     int alignedH = h;
377     uint32_t totalSize = surfaceBuffer->GetSize();
378     uint32_t seq = surfaceBuffer->GetSeqNum();
379     GraphicPixelFormat graphicFmt = static_cast<GraphicPixelFormat>(surfaceBuffer->GetFormat());
380     std::optional<PixelFmt> fmt = TypeConverter::GraphicFmtToFmt(graphicFmt);
381     IF_TRUE_RETURN_VOID_WITH_MSG(!fmt.has_value(), "unknown fmt %d", graphicFmt);
382 
383     string suffix;
384     bool dumpAsVideo = true;
385     DecideDumpInfo(alignedH, totalSize, suffix, dumpAsVideo);
386 
387     char name[128];
388     int ret = 0;
389     if (dumpAsVideo) {
390         ret = sprintf_s(name, sizeof(name), "%s/%s_%dx%d(%dx%d)_fmt%s.%s",
391                         DUMP_PATH, prefix.c_str(), w, h, byteStride, alignedH,
392                         fmt->strFmt.c_str(), suffix.c_str());
393     } else {
394         ret = sprintf_s(name, sizeof(name), "%s/%s_%" PRIu64 "_%dx%d(%d)_fmt%s_pts%" PRId64 "_seq%u.%s",
395                         DUMP_PATH, prefix.c_str(), cnt, w, h, byteStride,
396                         fmt->strFmt.c_str(), omxBuffer->pts, seq, suffix.c_str());
397     }
398     if (ret > 0) {
399         ofstream ofs(name, ios::binary | ios::app);
400         if (ofs.is_open()) {
401             ofs.write(va, totalSize);
402         } else {
403             LOGW("cannot open %s", name);
404         }
405     }
406     // if we unmap here, flush cache will fail
407 }
408 
DecideDumpInfo(int& alignedH, uint32_t& totalSize, string& suffix, bool& dumpAsVideo) const409 void HCodec::BufferInfo::DecideDumpInfo(int& alignedH, uint32_t& totalSize, string& suffix, bool& dumpAsVideo) const
410 {
411     int h = surfaceBuffer->GetHeight();
412     int byteStride = surfaceBuffer->GetStride();
413     GraphicPixelFormat fmt = static_cast<GraphicPixelFormat>(surfaceBuffer->GetFormat());
414     switch (fmt) {
415         case GRAPHIC_PIXEL_FMT_YCBCR_420_P:
416         case GRAPHIC_PIXEL_FMT_YCRCB_420_SP:
417         case GRAPHIC_PIXEL_FMT_YCBCR_420_SP:
418         case GRAPHIC_PIXEL_FMT_YCBCR_P010:
419         case GRAPHIC_PIXEL_FMT_YCRCB_P010: {
420             OH_NativeBuffer_Planes *planes = nullptr;
421             GSError err = surfaceBuffer->GetPlanesInfo(reinterpret_cast<void**>(&planes));
422             if (err != GSERROR_OK || planes == nullptr) { // compressed
423                 suffix = "bin";
424                 dumpAsVideo = false;
425                 return;
426             }
427             alignedH = static_cast<int32_t>(static_cast<int64_t>(planes->planes[1].offset) / byteStride);
428             totalSize = GetYuv420Size(byteStride, alignedH);
429             suffix = "yuv";
430             break;
431         }
432         case GRAPHIC_PIXEL_FMT_RGBA_8888: {
433             totalSize = static_cast<uint32_t>(byteStride * h);
434             suffix = "rgba";
435             break;
436         }
437         default: {
438             suffix = "bin";
439             dumpAsVideo = false;
440             break;
441         }
442     }
443 }
444 
DumpLinearBuffer(const string& prefix) const445 void HCodec::BufferInfo::DumpLinearBuffer(const string& prefix) const
446 {
447     if (omxBuffer->filledLen == 0) {
448         return;
449     }
450     if (avBuffer == nullptr || avBuffer->memory_ == nullptr) {
451         LOGW("invalid avbuffer");
452         return;
453     }
454     const char* va = reinterpret_cast<const char*>(avBuffer->memory_->GetAddr());
455     if (va == nullptr) {
456         LOGW("null va");
457         return;
458     }
459 
460     char name[128];
461     int ret = sprintf_s(name, sizeof(name), "%s/%s.bin", DUMP_PATH, prefix.c_str());
462     if (ret <= 0) {
463         LOGW("sprintf_s failed");
464         return;
465     }
466     ofstream ofs(name, ios::binary | ios::app);
467     if (ofs.is_open()) {
468         ofs.write(va, omxBuffer->filledLen);
469     } else {
470         LOGW("cannot open %s", name);
471     }
472 }
473 #endif // BUILD_ENG_VERSION
474 }