Skip to content

Commit

Permalink
SetPixels Analyzer (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
sailro authored May 20, 2020
1 parent 37f6bbb commit e1efba6
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 23 deletions.
44 changes: 44 additions & 0 deletions doc/UNT0017.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# UNT0017 SetPixels invocation is slow

Unity is using two distinct representations for a RGBA Color:
* [Color](https://docs.unity3d.com/ScriptReference/Color.html): Each color component is a **floating point value** with a range from 0 to 1. (this format is used inside all graphics cards and shaders).
* [Color32](https://docs.unity3d.com/ScriptReference/Color32.html): Each color component is a **byte value** with a range from 0 to 255. (32bit RGBA).

`Color32` is much faster and uses 4X less memory. `Color` and `Color32` can be implicitly converted to each other.

Compared to `SetPixels`, `SetPixels32` is much faster and uses less memory.

## Examples of patterns that are flagged by this analyzer

```csharp
using UnityEngine;

public class ExampleClass : MonoBehaviour
{
void Start()
{
Renderer rend = GetComponent<Renderer>();
Texture2D texture = Instantiate(rend.material.mainTexture) as Texture2D;
rend.material.mainTexture = texture;

// ...
Color[] colors = new Color[3];
colors[0] = Color.red;
colors[1] = Color.green;
colors[2] = Color.blue;
texture.SetPixels(colors);

// ...
}
}
```

## Solution

If 32bit-RGBA is compatible with your scenario, use `SetPixels32` instead.

Note: sometimes though 32bit-RGBA is not enough (like for HDR, Dolby Vision or advanced color manipulation).

No automatic code fix is available for this diagnostic.
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ID | Title | Category
[UNT0014](UNT0014.md) | GetComponent called with non-Component or non-Interface type | Type Safety
[UNT0015](UNT0015.md) | Incorrect method signature with InitializeOnLoadMethod or RuntimeInitializeOnLoadMethod attribute | Type Safety
[UNT0016](UNT0016.md) | Unsafe way to get the method name | Type Safety
[UNT0017](UNT0017.md) | SetPixels invocation is slow | Performance

# Diagnostic Suppressors

Expand Down
102 changes: 102 additions & 0 deletions src/Microsoft.Unity.Analyzers.Tests/SetPixelsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*-------------------------------------------------------------------------------------------*/

using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Unity.Analyzers.Tests
{
public class SetPixelsTests : BaseDiagnosticVerifierTest<SetPixelsMethodUsageAnalyzer>
{
[Fact]
public async Task Texture2DTest()
{
const string test = @"
using UnityEngine;
class Camera : MonoBehaviour
{
private void Test(Texture2D test)
{
test.SetPixels(null);
}
}
";

var diagnostic = ExpectDiagnostic()
.WithLocation(8, 14)
.WithArguments("SetPixels");

await VerifyCSharpDiagnosticAsync(test, diagnostic);
}

[Fact]
public async Task Texture3DTest()
{
const string test = @"
using UnityEngine;
class Camera : MonoBehaviour
{
private void Test(Texture3D test)
{
test.SetPixels(null);
}
}
";

var diagnostic = ExpectDiagnostic()
.WithLocation(8, 14)
.WithArguments("SetPixels");

await VerifyCSharpDiagnosticAsync(test, diagnostic);
}

[Fact]
public async Task CubemapArrayTest()
{
const string test = @"
using UnityEngine;
class Camera : MonoBehaviour
{
private void Test(CubemapArray test)
{
test.SetPixels(null, CubemapFace.Unknown, 0);
}
}
";

var diagnostic = ExpectDiagnostic()
.WithLocation(8, 14)
.WithArguments("SetPixels");

await VerifyCSharpDiagnosticAsync(test, diagnostic);
}

[Fact]
public async Task Texture2DArrayTest()
{
const string test = @"
using UnityEngine;
class Camera : MonoBehaviour
{
private void Test(Texture2DArray test)
{
test.SetPixels(null, 0);
}
}
";

var diagnostic = ExpectDiagnostic()
.WithLocation(8, 14)
.WithArguments("SetPixels");

await VerifyCSharpDiagnosticAsync(test, diagnostic);
}

}
}
33 changes: 33 additions & 0 deletions src/Microsoft.Unity.Analyzers/MethodSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*-------------------------------------------------------------------------------------------*/
using System.Reflection;
using Microsoft.CodeAnalysis;

namespace Microsoft.Unity.Analyzers
{
internal static class MethodSymbolExtensions
{
public static bool Matches(this IMethodSymbol symbol, MethodInfo method)
{
if (method.Name != symbol.Name)
return false;

if (!symbol.ReturnType.Matches(method.ReturnType))
return false;

var parameters = method.GetParameters();
if (parameters.Length < symbol.Parameters.Length)
return false;

for (var i = 0; i < symbol.Parameters.Length; i++)
{
if (!symbol.Parameters[i].Type.Matches(parameters[i].ParameterType))
return false;
}

return true;
}
}
}
84 changes: 84 additions & 0 deletions src/Microsoft.Unity.Analyzers/MethodUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*-------------------------------------------------------------------------------------------*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.Unity.Analyzers
{
[AttributeUsage(AttributeTargets.Method)]
public class MethodUsageAttribute : Attribute
{
}

public abstract class MethodUsageAnalyzer<T> : DiagnosticAnalyzer where T : MethodUsageAttribute
{
private static ILookup<string, MethodInfo> _lookup;

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}

protected virtual bool IsReportable(IMethodSymbol method)
{
if (_lookup == null)
{
_lookup = CollectMethods()
.Where(m => m.DeclaringType != null)
.ToLookup(m => m.DeclaringType.FullName);
}

// lookup returns an empty collection for nonexistent keys
var typename = method.ContainingType.ToDisplayString();
return _lookup[typename].Any(method.Matches);
}

protected virtual IEnumerable<MethodInfo> CollectMethods()
{
return CollectMethods(GetType().Assembly);
}

protected static IEnumerable<MethodInfo> CollectMethods(params Type[] types)
{
return types
.SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
.Where(m => m.GetCustomAttributes(typeof(T), true).Length > 0);
}

protected static IEnumerable<MethodInfo> CollectMethods(Assembly assembly)
{
return CollectMethods(assembly.GetTypes());
}

private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;

if (!(invocation.Expression is MemberAccessExpressionSyntax member))
return;

var symbol = context.SemanticModel.GetSymbolInfo(member);
if (symbol.Symbol == null)
return;

if (!(symbol.Symbol is IMethodSymbol method))
return;

if (!IsReportable(method))
return;

context.ReportDiagnostic(Diagnostic.Create(SupportedDiagnostics.First(), member.Name.GetLocation(), method.Name));
}
}
}
27 changes: 27 additions & 0 deletions src/Microsoft.Unity.Analyzers/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Microsoft.Unity.Analyzers/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,14 @@
<data name="InitializeOnLoadMethodDiagnosticTitle" xml:space="preserve">
<value>Use a static and parameterless method</value>
</data>
<data name="SetPixelsMethodUsageDiagnosticDescription" xml:space="preserve">
<value>Compared to SetPixels, SetPixels32 is much faster and uses less memory</value>
</data>
<data name="SetPixelsMethodUsageDiagnosticMessageFormat" xml:space="preserve">
<value>{0} is slower than SetPixels32</value>
<comment>{0} is a method name</comment>
</data>
<data name="SetPixelsMethodUsageDiagnosticTitle" xml:space="preserve">
<value>SetPixels invocation is slow</value>
</data>
</root>
25 changes: 2 additions & 23 deletions src/Microsoft.Unity.Analyzers/ScriptInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,37 +77,16 @@ private bool IsImplemented(MethodInfo method)
if (!(member is IMethodSymbol methodSymbol))
continue;

if (MethodMatch(method, methodSymbol))
if (methodSymbol.Matches(method))
return true;
}

return false;
}

private static bool MethodMatch(MethodInfo method, IMethodSymbol symbol)
{
if (method.Name != symbol.Name)
return false;

if (!symbol.ReturnType.Matches(method.ReturnType))
return false;

var parameters = method.GetParameters();
if (parameters.Length < symbol.Parameters.Length)
return false;

for (var i = 0; i < symbol.Parameters.Length; i++)
{
if (!symbol.Parameters[i].Type.Matches(parameters[i].ParameterType))
return false;
}

return true;
}

public bool IsMessage(IMethodSymbol method)
{
return GetMessages().Any(message => MethodMatch(message, method));
return GetMessages().Any(method.Matches);
}

private static Type GetMatchingMetadata(ITypeSymbol symbol)
Expand Down
Loading

0 comments on commit e1efba6

Please sign in to comment.