pgadmin3/ctl/explainShape.cpp
lsv 5aeb93b625 Optimization for linux.
Исправлено форматирование и учтены особенности GTK для корректного
отображения Log View.
2025-03-27 22:08:54 +05:00

711 lines
23 KiB
C++

//////////////////////////////////////////////////////////////////////////
//
// pgAdmin III - PostgreSQL Tools
//
// Copyright (C) 2002 - 2016, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
// explainShape.cpp - Explain Shapes
//
//////////////////////////////////////////////////////////////////////////
// wxWindows headers
#include <wx/wx.h>
// App headers
#include "pgAdmin3.h"
#include "ctl/explainCanvas.h"
#include <wx/docview.h>
#include "images/ex_aggregate.pngc"
#include "images/ex_partial_aggregate.pngc"
#include "images/ex_finalize_aggregate.pngc"
#include "images/ex_append.pngc"
#include "images/ex_bmp_and.pngc"
#include "images/ex_bmp_heap.pngc"
#include "images/ex_bmp_index.pngc"
#include "images/ex_bmp_or.pngc"
#include "images/ex_cte_scan.pngc"
#include "images/ex_delete.pngc"
#include "images/ex_foreign_scan.pngc"
#include "images/ex_group.pngc"
#include "images/ex_hash.pngc"
#include "images/ex_hash_anti_join.pngc"
#include "images/ex_hash_semi_join.pngc"
#include "images/ex_hash_setop_except.pngc"
#include "images/ex_hash_setop_except_all.pngc"
#include "images/ex_hash_setop_intersect.pngc"
#include "images/ex_hash_setop_intersect_all.pngc"
#include "images/ex_hash_setop_unknown.pngc"
#include "images/ex_index_only_scan.pngc"
#include "images/ex_index_scan.pngc"
#include "images/ex_insert.pngc"
#include "images/ex_lock_rows.pngc"
#include "images/ex_join.pngc"
#include "images/ex_limit.pngc"
#include "images/ex_materialize.pngc"
#include "images/ex_merge.pngc"
#include "images/ex_merge_anti_join.pngc"
#include "images/ex_merge_append.pngc"
#include "images/ex_merge_semi_join.pngc"
#include "images/ex_gatcher.pngc"
#include "images/ex_nested.pngc"
#include "images/ex_nested_loop_anti_join.pngc"
#include "images/ex_nested_loop_semi_join.pngc"
#include "images/ex_recursive_union.pngc"
#include "images/ex_result.pngc"
#include "images/ex_scan.pngc"
#include "images/ex_pscan.pngc"
#include "images/ex_seek.pngc"
#include "images/ex_setop.pngc"
#include "images/ex_sort.pngc"
#include "images/ex_subplan.pngc"
#include "images/ex_tid_scan.pngc"
#include "images/ex_unique.pngc"
#include "images/ex_unknown.pngc"
#include "images/ex_update.pngc"
#include "images/ex_values_scan.pngc"
#include "images/ex_window_aggregate.pngc"
#include "images/ex_worktable_scan.pngc"
// Greenplum images
#include "images/ex_broadcast_motion.pngc"
#include "images/ex_redistribute_motion.pngc"
#include "images/ex_gather_motion.pngc"
#define BMP_BORDER 3
ExplainShape::ExplainShape(const wxImage &bmp, const wxString &description, long tokenNo, long detailNo)
{
SetBitmap(wxBitmap(bmp));
SetLabel(description, tokenNo, detailNo);
SetDisableLabel(true);
kidCount = 0;
totalShapes = 0;
usedShapes = 0;
m_rootShape = false;
}
void ExplainShape::SetLabel(const wxString &str, int tokenNo, int detailNo)
{
if (tokenNo < 0)
{
description = str;
label = str;
}
else
{
wxStringTokenizer tokens(str, wxT(" "));
while (tokenNo-- >= 0)
{
label = tokens.GetNextToken();
if (detailNo <= 0)
{
if (!description.IsEmpty())
description.Append(wxT(" "));
description.Append(label);
}
}
if (detailNo > 0)
{
tokens.SetString(str, wxT(" "));
while (detailNo--)
{
if (!description.IsEmpty())
description.Append(wxT(" "));
description.Append(tokens.GetNextToken());
}
detail = tokens.GetString();
}
}
}
typedef struct autospacesolver {
int flags = 0;
wxSize min{-1,-1};
wxSize max;
} autospacesolver;
void ExplainShape::OnDraw(wxDC &dc)
{
wxBitmap &bmp = GetBitmap();
if (!bmp.Ok())
return;
// We do not draw the root shape
if (m_rootShape)
return;
wxRect trg=GetCanvas()->GetClientRect();
wxMouseEvent ev;
ev.m_x = 0;
ev.m_y = 0;
wxPoint logPos(ev.GetLogicalPosition(dc));
ev.m_x = trg.x+trg.width;
ev.m_y = trg.height;
wxPoint logPos2(ev.GetLogicalPosition(dc));
wxRect cliprect(logPos, logPos2);
double width = 0.0, height = 0.0;
GetBoundingBoxMin(&width,&height);
wxRect sp((int)(GetX()), (int)(GetY()), (int) (width), (int)(height));
//int x = (int)(GetX() + (width ));
//int y = (int)(GetY() + (height));
if (!cliprect.Intersects(sp)) {
if (dynamic_cast<ExplainCanvas *>(GetCanvas())->isneedoptimizedraw) return;
}
// dynamic bitmap
bool bmpdynamic = false;
wxBitmap bmpm(bmp.GetWidth(),bmp.GetHeight());
if (label.StartsWith("Memoize")) {
wxPen p = dc.GetPen();
wxMemoryDC dc2;
dc2.SelectObject(bmpm);
//dc2.SetBackgroundMode(wxBRUSHSTYLE_SOLID);
dc2.SetBrush(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)));
dc2.Clear();
int w = bmpm.GetWidth();
int h = bmpm.GetHeight();
dc2.DrawRoundedRectangle(wxRect(0, 0, w, h),2);
//dc2.DrawRectangle(0, 0, w, h);
wxRegEx r("Hits: (\\d+) Misses: (\\d+) Evictions: (\\d+) Overflows: (\\d+) Memory Usage: (\\d+\\S+)");
long hit=0, mis=0, evi=0, ovr=0;
wxString mem;
int x = 0, y = 0;
int tw, th;
wxString txt = _("Cache");
wxFont f(GetCanvas()->GetFont());
f.SetPointSize(8);
mem = f.GetNativeFontInfoDesc();
dc2.SetFont(f);
dc2.SetPen(p);
//dc2.SetTextForeground(*wxBLACK);
//dc2.SetTextBackground(*wxWHITE);
dc2.GetTextExtent(txt, &tw, &th);
int border = 2;
y += border;
x = (w - tw) / 2;
dc2.DrawText(txt, x, y);
y += th+ border;
//dc2.DrawText("TEst", 0, 0);
mem = wxEmptyString;
int spaceV = h-y; // label Cache
int rr = 4;
int h_line = th / 2;
if (r.Matches(condition)) {
hit=StrToLong( r.GetMatch(condition, 1));
mis = StrToLong(r.GetMatch(condition, 2));
evi = StrToLong(r.GetMatch(condition, 3));
ovr = StrToLong(r.GetMatch(condition, 4));
mem = (r.GetMatch(condition, 5));
spaceV = spaceV - th; // memory usage
if (evi || ovr) {
spaceV = spaceV - 2 * rr;
if (spaceV < 8) {
border = 1; h_line = 6; rr = 3;
}
}
}
wxRect rect(border, y, w - 2 * border, h_line);
//dc2.DrawRectangle(0, 0, w, h);
if ((mis + hit) > 0) {
int hitwidth = rect.width * hit * 1.0 / (mis + hit);
{
wxDCPenChanger ppen(dc2, *wxTRANSPARENT_PEN);
//wxColour c(wxString("#00FF00"));
wxColour c(*wxGREEN);
wxDCBrushChanger br(dc2, wxBrush(c));
//dc2.SetBrush(*wxGREEN_BRUSH);
dc2.DrawRectangle(BMP_BORDER, y, hitwidth, rect.height);
dc2.SetPen(dc.GetPen());
dc2.SetBrush(*wxTRANSPARENT_BRUSH);
dc2.DrawRoundedRectangle(rect,(double)2.0);
}
y += rect.height + border;
}
else {
wxDCBrushChanger br(dc2, wxBrush(p.GetColour(), wxHATCHSTYLE_BDIAGONAL));
rect.x = 0; rect.width = w; rect.height = h - y;
dc2.DrawRectangle(rect);
}
if (!mem.IsEmpty()) {
mem = "" + mem;
dc2.DrawText(mem, border, y);
y += th;
}
x = border;
if (evi > 0) {
wxDCBrushChanger br(dc2, wxBrush(*wxBLUE));
wxPoint p(x+rr,y+rr);
dc2.DrawCircle(p, rr);
//y + 2 * r + 1;
x += 2 * rr + rr;
}
if (ovr > 0) {
wxDCBrushChanger br(dc2, wxBrush(*wxRED));
wxPoint p(x + rr, y + rr);
dc2.DrawCircle(p, rr);
x += 2 * rr + 3;
}
{
wxPen p = dc.GetPen();
wxDCPenChanger ppen(dc2, wxPen(p.GetColour(), 2));
//dc2.DrawLine(wxPoint(0, 0), wxPoint(w, h));
//dc2.DrawLine(wxPoint(w, 0), wxPoint(0, h));
}
dc2.SelectObject(wxNullBitmap);
bmpdynamic = true;
}
int x, y;
x = WXROUND(m_xpos - bmp.GetWidth() / 2.0);
y = WXROUND(m_ypos - GetHeight() / 2.0);
if (bmpdynamic) dc.DrawBitmap(bmpm, x, y, true); else dc.DrawBitmap(bmp, x, y, true);
int w, h;
dc.SetFont(GetCanvas()->GetFont());
dc.GetTextExtent(label.Trim(), &w, &h);
x = WXROUND(m_xpos - w / 2.0);
y += bmp.GetHeight() + BMP_BORDER;
dc.DrawText(label.Trim(), x, y);
}
void ExplainShape::OnLeftClick(double x, double y, int keys, int attachment)
{
((ExplainCanvas *)GetCanvas())->ShowPopup(this);
}
#define ARROWMARGIN 5
wxRealPoint ExplainShape::GetStartPoint()
{
wxRealPoint rp(GetX() + GetBitmap().GetWidth() / 2.0 + ARROWMARGIN, GetY() - (GetHeight() - GetBitmap().GetHeight()) / 2.);
return rp;
}
wxRealPoint ExplainShape::GetEndPoint(int kidNo)
{
wxRealPoint rp(GetX() - GetBitmap().GetWidth() / 2.0 - ARROWMARGIN, GetY() - (GetHeight() - GetBitmap().GetHeight()) / 2. + (kidCount > 1 ? GetBitmap().GetHeight() * 2. / 3. * kidNo / (2 * kidCount - 2) : 0 ));
return rp;
}
ExplainShape *ExplainShape::Create(long level, ExplainShape *last, const wxString &str)
{
ExplainShape *s = 0;
int costPos = str.Find(wxT("(cost="));
int actPos = str.Find(wxT("(actual"));
bool parallel=false;
int inc=0;
wxStringTokenizer tokens(str, wxT(" "));
wxString token = tokens.GetNextToken();
if (token == wxT("Parallel"))
{
token = tokens.GetNextToken();
parallel=true;
inc=1;
}
wxString token2 = tokens.GetNextToken();
wxString token3 = tokens.GetNextToken();
wxString token4;
if (tokens.HasMoreTokens())
token4 = tokens.GetNextToken();
wxString descr;
if (costPos > 0)
descr = str.Left(costPos);
else if (actPos > 0)
descr = str.Left(actPos);
else
descr = str;
// Requested an empty shape, which can be treated as a root shape
if (level == 0)
{
s = new ExplainShape(*ex_unknown_png_img, wxEmptyString);
s->SetDraggable(false);
s->m_rootShape = true;
s->level = level;
int w = 50, h = 20;
wxBitmap &bmp = s->GetBitmap();
if (w < bmp.GetWidth())
w = bmp.GetWidth();
s->SetHeight(bmp.GetHeight() + BMP_BORDER + h);
s->SetWidth(w);
s->upperShape = NULL;
s->kidNo = 0;
return s;
}
// possible keywords can be found in postgresql/src/backend/commands/explain.c
if (token == wxT("Total")) return 0;
else if (token == wxT("Trigger")) return 0;
else if (token == wxT("Settings:")) return 0; /* Greenplum */
else if (token == wxT("Slice")) return 0; /* Greenplum */
else if (token.Mid(0, 6) == wxT("(slice")) return 0; /* Greenplum */
else if (token == wxT("Result")) s = new ExplainShape(*ex_result_png_img, descr);
else if (token == wxT("Append")) s = new ExplainShape(*ex_append_png_img, descr);
else if (token == wxT("Gather")) s = new ExplainShape(*ex_gatcher_png_img, descr);
else if (token == wxT("Partial") && token2 == wxT("GroupAggregate")) s = new ExplainShape(*ex_partial_aggregate_png_img, descr);
else if (token == wxT("Finalize") && token2 == wxT("GroupAggregate")) s = new ExplainShape(*ex_finalize_aggregate_png_img, descr);
else if (token == wxT("Nested"))
{
if (token2 == wxT("Loop") && token4 == wxT("Join"))
{
// Nested Loop Anti Join
if (token3 == wxT("Anti"))
{
s = new ExplainShape(*ex_nested_loop_anti_join_png_img, descr);
}
// Nested Loop Semi Join
else
{
s = new ExplainShape(*ex_nested_loop_semi_join_png_img, descr);
}
}
if (!s)
s = new ExplainShape(*ex_nested_png_img, descr);
}
else if (token == wxT("Merge"))
{
if (token3 == wxT("Join"))
{
// Merge Anti Join
if (token2 == wxT("Anti"))
{
s = new ExplainShape(*ex_merge_anti_join_png_img, descr);
}
// Merge Semi Join
else
{
s = new ExplainShape(*ex_merge_semi_join_png_img, descr);
}
}
// Merge Append
else if (token2 == wxT("Append"))
{
s = new ExplainShape(*ex_merge_append_png_img, descr);
}
else
{
s = new ExplainShape(*ex_merge_png_img, descr);
}
}
else if (token == wxT("Hash"))
{
if (token2 == wxT("Join"))
{
s = new ExplainShape(*ex_join_png_img, descr);
}
else
{
if (token3 == wxT("Join"))
{
// Hash Anti Join
if (token2 == wxT("Anti"))
{
s = new ExplainShape(*ex_hash_anti_join_png_img, descr);
}
// Hash Semi Join
else if (token2 == wxT("Semi"))
{
s = new ExplainShape(*ex_hash_semi_join_png_img, descr);
}
else
{
s = new ExplainShape(*ex_hash_png_img, descr);
}
}
else
{
s = new ExplainShape(*ex_hash_png_img, descr);
}
}
}
else if (token == wxT("HashSetOp"))
{
if (token2 == wxT("Except"))
{
// HashSetOp Except ALL
if (token3 == wxT("ALL"))
{
s = new ExplainShape(*ex_hash_setop_except_all_png_img, descr);
}
// HashSetOp Except
else
{
s = new ExplainShape(*ex_hash_setop_except_png_img, descr);
}
}
else if (token2 == wxT("Intersect"))
{
// HashSetOp Intersect ALL
if (token3 == wxT("ALL"))
{
s = new ExplainShape(*ex_hash_setop_intersect_all_png_img, descr);
}
// HashSetOp Intersect
else
{
s = new ExplainShape(*ex_hash_setop_intersect_png_img, descr);
}
}
else
{
// HashSetOp ???
s = new ExplainShape(*ex_hash_setop_unknown_png_img, descr);
}
}
else if (token == wxT("Subquery")) s = new ExplainShape(*ex_subplan_png_img, descr, 0, 2);
else if (token == wxT("Function")) s = new ExplainShape(*ex_result_png_img, descr, 0, 2);
else if (token == wxT("Materialize")) s = new ExplainShape(*ex_materialize_png_img, descr);
else if (token == wxT("Sort")) s = new ExplainShape(*ex_sort_png_img, descr);
else if (token == wxT("Group")) s = new ExplainShape(*ex_group_png_img, descr);
else if (token == wxT("Aggregate") || token == wxT("GroupAggregate") || token == wxT("HashAggregate"))
s = new ExplainShape(*ex_aggregate_png_img, descr);
else if (token == wxT("Unique")) s = new ExplainShape(*ex_unique_png_img, descr);
else if (token == wxT("SetOp")) s = new ExplainShape(*ex_setop_png_img, descr);
else if (token == wxT("Limit")) s = new ExplainShape(*ex_limit_png_img, descr);
else if (token == wxT("LockRows")) s = new ExplainShape(*ex_lock_rows_png_img, descr);
else if (token == wxT("Bitmap"))
{
if (token2 == wxT("Index")) s = new ExplainShape(*ex_bmp_index_png_img, descr, 4+inc, 3+inc);
else s = new ExplainShape(*ex_bmp_heap_png_img, descr, 4+inc, 3+inc);
}
else if (token == wxT("BitmapAnd")) s = new ExplainShape(*ex_bmp_and_png_img, descr);
else if (token == wxT("BitmapOr")) s = new ExplainShape(*ex_bmp_or_png_img, descr);
else if (token2 == wxT("Scan"))
{
if (token == wxT("Index"))
// Scan Index Backward
if (token3 == wxT("Backward"))
s = new ExplainShape(*ex_index_scan_png_img, descr, 4+inc, 3+inc);
else
s = new ExplainShape(*ex_index_scan_png_img, descr, 3+inc, 2+inc);
// Tid Scan
else if (token == wxT("Tid"))
s = new ExplainShape(*ex_tid_scan_png_img, descr, 3+inc, 2+inc);
// WorkTable Scan
else if (token == wxT("WorkTable"))
s = new ExplainShape(*ex_worktable_scan_png_img, descr, 3+inc, 2+inc);
// CTE Scan
else if (token == wxT("CTE"))
s = new ExplainShape(*ex_cte_scan_png_img, descr, 3, 2);
// Foreign Scan
else if (token == wxT("Foreign"))
s = new ExplainShape(*ex_foreign_scan_png_img, descr, 3, 2);
// Values Scan
else if (token == wxT("Values"))
s = new ExplainShape(*ex_values_scan_png_img, descr, 3, 2);
else if (parallel) s = new ExplainShape(*ex_pscan_png_img, descr, 4, 3);
else
s = new ExplainShape(*ex_scan_png_img, descr, 3+inc, 2+inc);
}
else if (token == wxT("Index"))
{
// Index Only Scan
if (token2 == wxT("Only") && token3 == wxT("Scan"))
{
s = new ExplainShape(*ex_index_only_scan_png_img, descr, 4+inc, 3+inc);
}
}
else if (token2 == wxT("Seek")) s = new ExplainShape(*ex_seek_png_img, descr, 3, 2);
// Recursive Union
else if (token == wxT("Recursive") && token2 == wxT("Union"))
s = new ExplainShape(*ex_recursive_union_png_img, descr);
else if (token == wxT("WindowAgg"))
s = new ExplainShape(*ex_window_aggregate_png_img, descr);
// DML
else if (token == wxT("Insert"))
s = new ExplainShape(*ex_insert_png_img, descr, 2, 1);
else if (token == wxT("Update"))
s = new ExplainShape(*ex_update_png_img, descr, 2, 1);
else if (token == wxT("Delete"))
s = new ExplainShape(*ex_delete_png_img, descr, 2, 1);
// Greenplum additions
else if (token == wxT("Gather") && token2 == wxT("Motion"))
s = new ExplainShape(*ex_gather_motion_png_img, descr);
else if (token == wxT("Broadcast") && token2 == wxT("Motion"))
s = new ExplainShape(*ex_broadcast_motion_png_img, descr);
else if (token == wxT("Redistribute") && token2 == wxT("Motion"))
s = new ExplainShape(*ex_redistribute_motion_png_img, descr);
if (!s)
s = new ExplainShape(*ex_unknown_png_img, descr);
s->SetDraggable(false);
s->level = level;
if (costPos > 0)
{
if (actPos > 0)
{
s->actual = str.Mid(actPos);
s->cost = str.Mid(costPos, actPos - costPos);
}
else
s->cost = str.Mid(costPos);
}
else if (actPos > 0)
s->actual = str.Mid(actPos);
int w = 50, h = 20;
wxBitmap &bmp = s->GetBitmap();
if (w < bmp.GetWidth())
w = bmp.GetWidth();
s->SetHeight(bmp.GetHeight() + BMP_BORDER + h);
s->SetWidth(w);
s->upperShape = last;
if (last)
{
s->kidNo = last->kidCount;
last->kidCount++;
}
else
s->kidNo = 0;
if (costPos > 0)
{
wxChar *cl = const_cast<wxChar *>((const wxChar *)str + costPos + 6);
wxChar *ch = wxStrstr(cl, wxT(".."));
if (ch)
{
*ch = 0;
ch += 2;
}
s->costLow = StrToDouble(cl);
if (ch)
{
wxChar *r = wxStrstr(ch, wxT(" rows="));
if (r)
{
*r = 0;
r += 6;
}
s->costHigh = StrToDouble(ch);
if (r)
{
wxChar *w = wxStrstr(r, wxT(" width="));
if (w)
{
*w = 0;
w += 7;
}
s->rows = StrToLong(r);
if (w)
s->width = StrToLong(w);
}
}
}
return s;
}
ExplainLine::ExplainLine(ExplainShape *from, ExplainShape *to, double weight)
{
SetCanvas(from->GetCanvas());
from->AddLine(this, to);
MakeLineControlPoints(4);
width = (int) log(from->GetAverageCost());
if (width > 10)
width = 10;
// If we got an average cost of 0, width is probably negative
// which will look pretty darn ugly as an arrow width!
// This may happen on Greenplum.
if (width < 1)
width = 1;
wxNode *first = GetLineControlPoints()->GetFirst();
wxNode *last = GetLineControlPoints()->GetLast();
*(wxRealPoint *)first->GetData() = from->GetStartPoint();
*(wxRealPoint *)last->GetData() = to->GetEndPoint(from->GetKidno());
wxRealPoint *p1 = (wxRealPoint *)first->GetNext()->GetData();
wxRealPoint *p2 = (wxRealPoint *)last->GetPrevious()->GetData();
*p1 = from->GetStartPoint();
*p2 = to->GetEndPoint(from->GetKidno());
p1->x -= (p1->x - p2->x) / 3. + 8;
p2->x += (p1->x - p2->x) / 3. - 8;
Initialise();
}
#define ARROWWIDTH 4
void ExplainLine::OnDraw(wxDC &dc)
{
if (m_lineControlPoints)
{
dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1, wxPENSTYLE_SOLID));
dc.SetBrush(*wxTheBrushList->FindOrCreateBrush(*wxLIGHT_GREY, wxBRUSHSTYLE_SOLID));
wxPoint *points = new wxPoint[11];
wxRealPoint *point0 = (wxRealPoint *) m_lineControlPoints->Item(0)->GetData();
wxRealPoint *point1 = (wxRealPoint *) m_lineControlPoints->Item(1)->GetData();
wxRealPoint *point2 = (wxRealPoint *) m_lineControlPoints->Item(2)->GetData();
wxRealPoint *point3 = (wxRealPoint *) m_lineControlPoints->Item(3)->GetData();
double phi = atan2(point2->y - point1->y, point2->x - point1->x);
double offs = width * tan(phi / 2);
points[0].x = WXROUND(point0->x);
points[0].y = WXROUND(point0->y) - width;
points[10].x = WXROUND(point0->x);
points[10].y = WXROUND(point0->y) + width;
points[1].x = WXROUND(point1->x + offs);
points[1].y = WXROUND(point1->y) - width;
points[9].x = WXROUND(point1->x - offs);
points[9].y = WXROUND(point1->y) + width;
points[2].x = WXROUND(point2->x + offs);
points[2].y = WXROUND(point2->y) - width;
points[8].x = WXROUND(point2->x - offs);
points[8].y = WXROUND(point2->y) + width;
points[3].x = WXROUND(point3->x) - width - ARROWWIDTH;
points[3].y = WXROUND(point3->y) - width;
points[7].x = WXROUND(point3->x) - width - ARROWWIDTH;
points[7].y = WXROUND(point3->y) + width;
points[4].x = points[3].x;
points[4].y = points[3].y - ARROWWIDTH;
points[6].x = points[7].x;
points[6].y = points[7].y + ARROWWIDTH;
points[5].x = WXROUND(point3->x);
points[5].y = WXROUND(point3->y);
dc.DrawPolygon(11, points, 0, 0);
}
}