In my previous article, I described OneLine - PropertyDrawer, which allows you to draw an object of any nesting in one line.
This time I will tell you how I had to optimize the code so that the inspector could edit databases consisting of hundreds of lines.
Attention, under the cat a lot of gifs and pictures!
In the standard inspector, all fields that have a complex structure are drawn collapsed, which consumes quite a few resources and allows you to easily draw an array consisting of hundreds of objects.
If we look at the profiler, we will see 4.3 ms to draw 100 elements of the array.
OneLine was invented to save the developer from constant mouse clicks in the inspector, and all the nested fields are drawn immediately. At the same time, during drawing, rather costly calculations of the element positions are performed.
Comrade Shipilev in his report "Performance: what is in my name to you?" proposes a graph of code performance versus its complexity (time moves along a curve from A to E):
This is a parametric graph: time flows here from point “A” to point “B”, “C”, “D”, “E”. On the y-axis we have performance, on the x-axis is some abstract complexity of the code.
Usually, it all starts with people cycling a prototype that works slowly but slowly. It is rather complicated because we just bike or not so that it does not fall apart under its own weight.
After the optimization begins - little by little the rewriting of different parts begins. Being in this green zone, developers usually take profilers and rewrite pieces of code that are obviously badly written. This at the same time reduces the complexity of the code (because you cut out bad pieces) and improves performance.
')
At point “B”, the project reaches a certain subjective peak of “beauty”, when we have a kind of performance and a good one, and the product is not bad.
Further, if developers want more performance, they go into the yellow zone when they take a more accurate profiler, write good workloads and carefully tighten the nuts. In this process, they do things there that they would not do if it were not for productivity.
If you want even further, then the project comes to a certain red zone, when developers begin to make their product go wild in order to get the last percentages of performance. What to do in this zone is not very clear. There is a recipe, at least for this conference - go to JPoint / JokerConf / JBreak and try product developers, how to write code that repeats the curvature of the lower layers. Because, as a rule, in the red zone there are things that repeat the problems that arise in the lower layers.
The schedule is extremely good, as the whole article, I highly recommend reading.
Moving from A to B is a rather boring and trivial thing. Our article covers first the movement from B to C, then - wandering around D in search of the right crutch / counterweight to the features of Unity.
Someone will always think: "What kind of kindergarten? And what do you call wandering around D? Where are the features of CLR and IL2CPP ? Where are the benchmarks according to all the rules? Where is the code over-complication to gain a speed of 0.05%?".
Most likely, the article is not for this reader. Things about which I write, are rather simple and are directed more likely to the young reader who is beginning the way of the developer in Unity. Therefore, I simplify the implementation and spend rather a description of the approaches to optimize PropertyDrawer.
Once I saw my colleague write PropertyDrawer, which displays a grid with a footprint of a game object. He generated images on the fly, pixel-by-pixel changing their colors and doing many more scary things. Ultimately, the display of one such field could significantly draw down the FPS in the editor.
Of course, this does not mean: "If someone does something stupid, it means I can." Just some nonsense, we are forced to do, and after - to optimize.
The same 100 elements required 104 ms, that is, ~ 24 times more. This speed can be described in one word: disgusting .
OneLine itself calculates the positions of the fields depending on their number, type and attributes hanging on them (for example, [Width]
or [Weight]
). And every call to OnGUI, this calculation happens again.
The same situation with the abstract PropertyDrawer, which works very slowly and therefore does not give rest to the programmer Vasya. Obviously, if slowly, then most likely it does a lot of identical calculations.
It was decided: we will cache!
To continue, we need to know the following: for one inspector window, only one PropertyDrawer object is created for each type, which draws all fields of this type, and after changing the object, it is discarded.
And this means that if we have two fields of the same type on the screen, they are drawn by the same PropertyDrawer object. This somewhat complicates caching.
On the other hand, if we have one PropertyDrower for one inspector, then we can save any data in a Dictionary<string, YourData>
, where property.propertyPath
key.
In the end, we get a simple cache:
public delegate <T> T CalculateValue (SerializedProperty property); public class PropertyDrawerCache<T> { private Dictionary<string, T> cache; private CalculateValue<T> calculate; public PropertyDrawerCache(CalculateValue<T> calculate){ cache = new Dictionary<string, T>(); this.calculate = calculate; } public T this[SerializedProperty property] { get { T result = null; if (cache.TryGetValue(string, out result)){ result = calculate(property); cache.Add(property.propertyPath, result); } return result; } } }
The first call is fulfilled as slowly, but all subsequent ones are twice as fast. Not bad, but it still feels unpleasant.
When first called:
On subsequent calls:
The main trouble cache: it must be kept up to date. We cache the calculated positions for the fields of the classes and consider them to be unchanged (the structure of the classes will not change in runtime). However, we did not take into account one thing: OneLine fits in the string as well all the elements of the child arrays.
Fortunately, the problem is solved by resetting the cached item positions.
public void DrawPlusButton(Rect rect, SerializedProperty array) { if (GUI.Button(rect, "+")) { array.InsertArrayElementAtIndex(array.arraySize); ResetCurrentElementCache(); } }
The main trouble cache: it must be kept up to date.
While using OneLine, I came across an interesting caching bug:
The same words:
Solution: remember the size for each array and with the next drawing check if the array has changed. I present the solution only for the case when OneLine hangs on an array in the root of the ScriptableObject to simplify reading.
At the same time, I draw the reader’s attention to the remarkable IsReallyArray
function in the code.
private Dictionary<string, int> arraysSizes = new Dictionary<string, int>(); public bool IsArraySizeChanged(SerializedProperty arrayElement){ var arrayName = arrayElement.propertyPath.Split('.')[0]; var array = arrayElement.serializedObject.FindProperty(arrayName); return IsReallyArray(array) && IsRealArraySizeChanged(arrayName, array.arraySize); } private bool IsReallyArray(this SerializedProperty property){ return property.isArray && property.propertyType != SerializedPropertyType.String; } private bool IsRealArraySizeChanged(string arrayName, int currentArraySize){ if (! arraysSizes.ContainsKey(arrayName)){ arraysSizes[arrayName] = currentArraySize; } else if (arraysSizes[arrayName] != currentArraySize){ arraysSizes[arrayName] = currentArraySize; return true; } return false; }
There are 100 elements in our array, and only 20-25 (on gifs) are visible on the screen, which brings us to another standard optimization: Culling : we just don’t draw something that doesn’t fit on the screen!
To do this, we need to know the size of the window in which we are located, as well as the position of the ScrollView. I offer you a solution on the verge of a foul (hello, Unity-Decompiled ).
internal class InspectorUtil { private const string INSPECTOR_WINDOW_ASSEMBLY_QUALIFIED_NAME = "UnityEditor.InspectorWindow, UnityEditor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"; private const string INITIALIZATION_ERROR_MESSAGE = @"OneLine can not initialize Inspector Window Utility. You may experience some performance issues. Please create an issue on https://github.com/slavniyteo/one-line/ and we will repair it. "; private bool enabled = true; private MethodInfo getWindowPositionInfo; private FieldInfo scrollPositionInfo; private object window; private float lastWindowWidth; public InspectorUtil() { try { Initialize(); enabled = true; } catch (Exception ex){ // // Unity , // . enabled = false; Debug.LogError(INITIALIZATION_ERROR_MESSAGE + ex.ToString()); } } private void Initialize(){ var inspectorWindowType = Type.GetType(INSPECTOR_WINDOW_ASSEMBLY_QUALIFIED_NAME); window = inspectorWindowType .GetField("s_CurrentInspectorWindow", BindingFlags.Public | BindingFlags.Static) .GetValue(null); scrollPositionInfo = inspectorWindowType .GetField("m_ScrollPosition"); getWindowPositionInfo = inspectorWindowType .GetProperty("position", typeof(Rect)) .GetGetMethod(); } public bool IsOutOfScreen(Rect position){ if (! enabled) { return false; } var scrollPosition = (Vector2) scrollPositionInfo.GetValue(window); var windowPosition = (Rect) getWindowPositionInfo.Invoke(window, null); bool above = (position.y + position.height) < scrollPosition.y; bool below = position.y > (scrollPosition.y + windowPosition.height); return above || below; } }
Then use:
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (inspectorUtil.IsOutOfScreen(position)) { return; } <..> }
This code will only work when objects are drawn in the inspector window. If you use OneLine (or another PropertyDrawer) in your custom window, this optimization will not work. The reason, of course, is the rigid basis for the scrolling of the screen. To make a universal tool in this case is impossible. But the code is quite simple, it can always be adapted to your needs.
It feels quite differently, the scrolling is much smoother, and the number of FPS largely depends on the height of the window.
If you look at the previous gif, you can see. that when scrolling, the focus of the element "sticks" to the top edge and does not leave the screen.
Obviously, Unity remembers the active element on the basis of its sequence number from the beginning of the event processing. And since culling discards all invisible elements, we are breaking this order.
Solving the problem can be quite simple: reset the focus from the control with each movement of the wheel.
if (Event.current.type == EventType.ScrollWheel){ EditorGUI.FocusTextInControl(""); }
But I have never integrated this solution into OneLine, because I have not yet convinced myself that this solution is good enough. Someday I dig a little deeper and maybe do it a little better.
When first called:
On subsequent calls, everything happens much faster:
For comparison, all options in one window:
As a result of the work, we received a significant acceleration of the library. I do not write "tenfold acceleration", since the time of drawing elements largely depends on the height of the window (if we take the window twice as long, the time will also increase almost twice). However, this result suits me.
I will not insert the block with advertising already familiar to Habré: whoever needs it will find it.
All good!
Source: https://habr.com/ru/post/341064/
All Articles