Skip to content

Commit d2d1b04

Browse files
committed
refactor: optimize, performance and the following
- remove old logic - public interface updates - text encoding option - follow attribute guideline - OverwriteIfFileExists is now enabled by default
1 parent fc86d66 commit d2d1b04

File tree

5 files changed

+88
-56
lines changed

5 files changed

+88
-56
lines changed

Editor/Attributes.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
using System;
2+
using System.Text;
23

34
namespace SatorImaging.UnitySourceGenerator
45
{
56
///<summary>NOTE: Implement "IUnitySourceGenerator" (C# 11.0)</summary>
67
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
78
public sealed class UnitySourceGeneratorAttribute : Attribute
89
{
10+
Type generatorClass;
11+
912
public UnitySourceGeneratorAttribute(Type generatorClass = null)
1013
{
11-
GeneratorClass = generatorClass;
14+
this.generatorClass = generatorClass;
1215
}
1316

14-
public bool OverwriteIfFileExists { get; set; } = false;
15-
public Type GeneratorClass { get; set; }
17+
public Type GeneratorClass => generatorClass;
18+
19+
public bool OverwriteIfFileExists { get; set; } = true;
20+
public Encoding OutputFileEncoding { get; set; } = Encoding.UTF8;
1621

1722
}
1823
}

Editor/USGEngine.cs

+54-43
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ namespace SatorImaging.UnitySourceGenerator
1515
{
1616
public class USGEngine : AssetPostprocessor
1717
{
18-
///<summary>This will be disabled after Unity Editor import event automatically.</summary>
18+
///<summary>This will be disabled automatically after Unity Editor import event.</summary>
1919
public static bool IgnoreOverwriteSettingByAttribute = false;
2020

2121

22+
const int BUFFER_LENGTH = 61_440;
23+
const int BUFFER_MAX_CHAR_LENGTH = BUFFER_LENGTH / 3; // worst case of UTF-8
2224
const string GENERATOR_PREFIX = ".";
2325
const string GENERATOR_EXT = ".g";
24-
const string GENERATOR_DIR = @"/USG.g"; // don't append last slash. this is used by .EndsWith()
26+
const string GENERATOR_DIR = @"/USG.g"; // don't append last slash. used to determine file is generated one or not.
2527
const string ASSETS_DIR_NAME = "Assets";
26-
const string ASSETS_DIR_SLASH = "Assets/";
28+
const string ASSETS_DIR_SLASH = ASSETS_DIR_NAME + "/";
2729
const string TARGET_FILE_EXT = @".cs";
2830
const string PATH_PREFIX_TO_IGNORE = @"Packages/";
2931
readonly static char[] DIR_SEPARATORS = new char[] { '\\', '/' };
3032

33+
// OPTIMIZE: Avoiding explicit static ctor is best practice for performance???
3134
readonly static string s_projectDirPath;
3235
static USGEngine()
3336
{
@@ -36,76 +39,67 @@ static USGEngine()
3639
s_projectDirPath = s_projectDirPath.Substring(0, s_projectDirPath.Length - ASSETS_DIR_NAME.Length);
3740
}
3841

39-
readonly static HashSet<string> s_targetFilePaths = new();
40-
static void AddAppropriateTarget(string filePath)
42+
static bool IsAppropriateTarget(string filePath)
4143
{
4244
if (!filePath.EndsWith(TARGET_FILE_EXT) ||
4345
!filePath.StartsWith(ASSETS_DIR_SLASH))
4446
{
45-
return;
47+
return false;
4648
}
47-
s_targetFilePaths.Add(filePath);
48-
}
49-
50-
51-
void OnPreprocessAsset()
52-
{
53-
AddAppropriateTarget(assetPath);
49+
return true;
5450
}
5551

5652

57-
// NOTE: To avoid event invoked twice on file deletion.
58-
static bool s_processingJobQueued = false;
59-
6053
static void OnPostprocessAllAssets(
6154
string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
6255
{
6356
// NOTE: Do NOT handle deleted assets because Unity tracking changes perfectly.
6457
// Even if delete file while Unity shutted down, asset deletion event happens on next Unity launch.
6558
// As a result, delete/import event loops infinitely and file cannot be deleted.
66-
for (int i = 0; i < importedAssets.Length; i++)
67-
{
68-
AddAppropriateTarget(importedAssets[i]);
69-
}
70-
71-
if (s_processingJobQueued) return;
72-
s_processingJobQueued = true;
7359

7460
// TODO: Unity sometimes reloads updated scripts by Visual Studio in background automatically.
7561
// In this situation, code generation will be done with script data right before saving.
7662
// It cannot be solved on C#, simply restart Unity.
7763
// Using [DidReloadScripts] or EditorApplication.delayCall, It works fine with Reimport
7864
// menu command but OnPostprocessAllAssets event doesn't work as expected.
79-
// (script runs with static field cleared even though .Clear() is only in ProcessingFiles())
65+
// (script runs with static field cleared even though .Clear() is only in ProcessingFiles().
66+
// it's weird that event happens and asset paths retrieved but hashset items gone.)
8067
////EditorApplication.delayCall += () =>
8168
{
82-
ProcessingFiles();
69+
ProcessingFiles(importedAssets);
8370
};
8471
}
8572

8673

8774
readonly static HashSet<string> s_updatedGeneratorNames = new();
88-
static void ProcessingFiles()
75+
static void ProcessingFiles(string[] targetPaths)
8976
{
9077
bool somethingUpdated = false;
91-
foreach (string path in s_targetFilePaths)
78+
for (int i = 0; i < targetPaths.Length; i++)
9279
{
93-
if (ProcessFile(path))
80+
// NOTE: Do NOT early return in this method.
81+
// check path here to allow generator class can be lie outside of Assets/ folder.
82+
if (!IsAppropriateTarget(targetPaths[i])) continue;
83+
84+
if (ProcessFile(targetPaths[i]))
9485
somethingUpdated = true;
9586
}
9687

9788
// TODO: more efficient way to process related targets
89+
var overwriteEnabledByCaller = IgnoreOverwriteSettingByAttribute;
9890
foreach (var generatorName in s_updatedGeneratorNames)
9991
{
10092
foreach (var info in s_typeNameToInfo.Values)
10193
{
102-
if (info.TargetClass == null) continue;
103-
if (info.Attribute.GeneratorClass?.Name != generatorName) continue;
94+
if (info.TargetClass == null ||
95+
info.Attribute.GeneratorClass?.Name != generatorName)
96+
continue;
10497

105-
var path = USGUtility.GetScriptFileByName(info.TargetClass.Name);
106-
if (path != null)
98+
var path = USGUtility.GetAssetPathByName(info.TargetClass.Name);
99+
if (path != null && IsAppropriateTarget(path))
107100
{
108-
IgnoreOverwriteSettingByAttribute = true;
101+
IgnoreOverwriteSettingByAttribute = overwriteEnabledByCaller
102+
|| info.Attribute.OverwriteIfFileExists;
109103
if (ProcessFile(path))
110104
somethingUpdated = true;
111105
}
@@ -114,11 +108,10 @@ static void ProcessingFiles()
114108

115109
if (somethingUpdated)
116110
AssetDatabase.Refresh();
117-
s_targetFilePaths.Clear();
111+
118112
s_updatedGeneratorNames.Clear();
119113

120114
IgnoreOverwriteSettingByAttribute = false; // always turn it off.
121-
s_processingJobQueued = false;
122115
}
123116

124117

@@ -176,15 +169,14 @@ public static bool ProcessFile(string assetsRelPath)
176169
outputPath = Path.Combine(outputPath, info.OutputFileName);
177170

178171

172+
// do it.
179173
var context = new USGContext
180174
{
181175
TargetClass = info.TargetClass,
182176
AssetPath = assetsRelPath.Replace('\\', '/'),
183177
OutputPath = outputPath.Replace('\\', '/'),
184178
};
185179

186-
187-
// do it.
188180
var sb = new StringBuilder();
189181
sb.AppendLine($"// <auto-generated>{generatorCls.Name}</auto-generated>");
190182

@@ -199,25 +191,44 @@ public static bool ProcessFile(string assetsRelPath)
199191
throw;
200192
}
201193

194+
//save??
202195
if (!isSaveFile || sb == null || string.IsNullOrWhiteSpace(context.OutputPath))
203196
return false;
204197

198+
if (File.Exists(context.OutputPath) &&
199+
(!info.Attribute.OverwriteIfFileExists && !IgnoreOverwriteSettingByAttribute)
200+
)
201+
{
202+
return false;
203+
}
205204

206205

207206
var outputDir = Path.GetDirectoryName(context.OutputPath);
208207
if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);
209208

210-
if (File.Exists(context.OutputPath) &&
211-
(!info.Attribute.OverwriteIfFileExists && !IgnoreOverwriteSettingByAttribute)
212-
)
209+
#if UNITY_2021_3_OR_NEWER
210+
211+
// OPTIMIZE: use sb.GetChunks() in future release of Unity. 2021 LTS doesn't support it.
212+
using (var fs = new FileStream(context.OutputPath, FileMode.Create, FileAccess.Write))
213213
{
214-
return false;
214+
Span<byte> buffer = stackalloc byte[BUFFER_LENGTH];
215+
var span = sb.ToString().AsSpan();
216+
for (int i = 0; i < span.Length; i += BUFFER_MAX_CHAR_LENGTH)
217+
{
218+
var len = BUFFER_MAX_CHAR_LENGTH;
219+
if (len + i > span.Length) len = span.Length - i;
220+
221+
int written = info.Attribute.OutputFileEncoding.GetBytes(span.Slice(i, len), buffer);
222+
fs.Write(buffer.Slice(0, written));
223+
}
224+
fs.Flush();
215225
}
216226

227+
#else
228+
File.WriteAllText(context.OutputPath, sb.ToString(), info.Attribute.OutputFileEncoding);
229+
#endif
217230

218-
File.WriteAllText(context.OutputPath, sb.ToString());
219231
Debug.Log($"[{nameof(UnitySourceGenerator)}] Generated: {context.OutputPath}");
220-
221232
return true;
222233
}
223234

Editor/USGUtility.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ static void ForceGenerateSelectedScripts()
2626
}
2727

2828

29-
public static void ForceGenerate(string clsName, bool showInProjectPanel = true)
29+
///<summary>UNSAFE on use in build event due to this method calls fancy UI methods and fire import event. Use `GetAssetPathByName()` instead.</summary>
30+
public static void ForceGenerateInEditor(string clsName, bool showInProjectPanel = true)
3031
{
31-
var path = GetScriptFileByName(clsName);
32+
var path = GetAssetPathByName(clsName);
3233
if (path == null) return;
3334

3435
if (showInProjectPanel)
@@ -38,7 +39,8 @@ public static void ForceGenerate(string clsName, bool showInProjectPanel = true)
3839
}
3940

4041

41-
internal static string GetScriptFileByName(string clsName)
42+
///<summary>Returns "Assets/" rooted path of the script file.</summary>
43+
public static string GetAssetPathByName(string clsName)
4244
{
4345
var GUIDs = AssetDatabase.FindAssets(clsName);
4446
foreach (var GUID in GUIDs)

README.md

+20-6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ As you already know, Roslyn's source generator is too sophisticated. This framew
3636
- [Copyright](#copyright)
3737
- [License](#license)
3838
- [Devnote](#devnote)
39+
- [TODO](#todo)
40+
- [Memo](#memo)
3941

4042
<!------- End of Details EN Tag -------></details></p>
4143

@@ -132,7 +134,7 @@ namespace Sample
132134

133135
Here is target-less generator example.
134136

135-
It is useful to generate static database that cannot be generated on Unity runtime. For example, asset GUIDs database, resource integrity tables, etc.
137+
It is useful to generate static database that cannot be generated on Unity runtime. For example, build asset GUIDs database using `UnityEditor.AssetDatabase`, resource integrity tables, etc.
136138

137139

138140

@@ -206,12 +208,12 @@ There are utility functions to perform source code generation on build event.
206208

207209

208210
```csharp
209-
// perform by known asset path.
210-
USGEngine.IgnoreOverwriteSettingByAttribute = true; // force overwrite
211-
USGEngine.ProcessFile(pathToGeneratorScriptFile);
211+
// search by class name if you don't know where it is.
212+
var assetPath = USGUtility.GetAssetPathByName(nameof(MinimalGenerator));
212213

213-
// search by class name.
214-
USGUtility.ForceGenerate(nameof(MinimalGenerator));
214+
// perform code generation.
215+
USGEngine.IgnoreOverwriteSettingByAttribute = true; // force overwrite
216+
USGEngine.ProcessFile(assetPath);
215217
```
216218

217219

@@ -355,4 +357,16 @@ SOFTWARE.
355357

356358
# Devnote
357359

360+
361+
## TODO
362+
363+
- Add new attribute option `UseCustomWriter` to use it's own file writer instead of builtin writer. For the "non-allocation" addicted developers.
364+
- `USGEngine.ProcessingFile()` doesn't care what happens in custom writer. just returns true in this situation.
365+
- Option is for generator class. Referenced generator class doesn't have `UnitySourceGenerator` attribute so that need to retrieve it from target classes. (how handle conflicts?)
366+
- `USGContext.UseCustomWriter` can be used to prevent writing file but `StringBuilder` is built prior to `Emit()` method.
367+
368+
369+
370+
## Memo
371+
358372
Unity doesn't invoke import event if Visual Studio is not launch by current session of Unity...?

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "com.sator-imaging.alt-source-generator",
33
"displayName": "Alternative Source Generator for Unity",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"unity": "2021.3",
66
"description": "Ease-of-Use Source Generator Alternative for Unity.",
77
"author": {

0 commit comments

Comments
 (0)