-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow emitting debugging symbols via high level scripting APIs #16489
Conversation
src/Scripting/Core/ScriptCompiler.cs
Outdated
@@ -3,6 +3,7 @@ | |||
using System; | |||
using System.Text; | |||
using System.Threading; | |||
using Microsoft.CodeAnalysis.Emit; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused?
src/Scripting/Core/ScriptOptions.cs
Outdated
|
||
namespace Microsoft.CodeAnalysis.Scripting | ||
{ | ||
using Microsoft.CodeAnalysis.Emit; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using Microsoft.CodeAnalysis.Emit; [](start = 4, length = 34)
Move outside the namespace
src/Scripting/Core/ScriptOptions.cs
Outdated
/// <summary> | ||
/// <see cref="DebugInformationFormat"/> to be used for debugging symbols if needed | ||
/// </summary> | ||
public DebugInformationFormat? DebugInformationFormat { get; private set; } = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DebugInformationFormat? [](start = 15, length = 23)
Nullable is not needed, we can use default(DebugInformationFormat). I guess we should add DebugInformationFormat.None to the enum -- it should be there per .NET Design Guidelines. #Resolved
src/Scripting/Core/ScriptOptions.cs
Outdated
/// <summary> | ||
/// <see cref="DebugInformationFormat"/> to be used for debugging symbols if needed | ||
/// </summary> | ||
public DebugInformationFormat? DebugInformationFormat { get; private set; } = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DebugInformationFormat? [](start = 15, length = 23)
Actually, I wonder if we need this option at all. We can detect whether the current platform supports Portable PDBs and only if it doesn't use Windows PDB. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like the best idea, I agree. I'm not sure what would be the determining factor though, is it similar to how it is the case for choosing the logic for assembly loading APIs? => if CoreCLR, use PortablePDB, otherwise use full ("properietary") PDB ? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question. Ideally we would use Windows PDB only when necessary. On CoreCLR and Mono we would always use Portable PDB. On Desktop FX, if we can detect 4.6.3+ we can use portable as well. 4.6.2 and below doesn't support portable. I'm not sure yet what would the best way to determine this be. Perhaps we can start with CoreCLR and Mono and use Windows PDB on Destkop FX and change it later. #Resolved
@@ -30,6 +30,14 @@ public override Assembly LoadFromStream(Stream peStream, Stream pdbStream) | |||
{ | |||
byte[] peImage = new byte[peStream.Length]; | |||
peStream.TryReadAll(peImage, 0, peImage.Length); | |||
|
|||
if (pdbStream != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ [](start = 35, length = 1)
Style: { on the next line #Resolved
src/Scripting/Core/ScriptOptions.cs
Outdated
/// <summary> | ||
/// Specifies the encoding to be used for the code. | ||
/// </summary> | ||
public Encoding Encoding { get; private set; } = Encoding.UTF8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encoding [](start = 24, length = 8)
I'd call it FileEncoding and explained in the comment that this is only relevant for debugging scripts that were read from a file or they will be saved to a file for debugging purposes.
src/Scripting/Core/ScriptBuilder.cs
Outdated
@@ -115,20 +115,28 @@ private static void ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, Diagn | |||
/// </summary> | |||
private Func<object[], Task<T>> Build<T>( | |||
Compilation compilation, | |||
DiagnosticBag diagnostics, | |||
DiagnosticBag diagnostics, ScriptOptions scriptOptions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ScriptOptions scriptOptions, [](start = 38, length = 29)
Style: on a new line. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd only pass DebugInformationFormat instead of all options to this method because that's the only thing needed here.
In reply to: 96129988 [](ancestors = 96129988)
src/Scripting/Core/ScriptBuilder.cs
Outdated
CancellationToken cancellationToken) | ||
{ | ||
var entryPoint = compilation.GetEntryPoint(cancellationToken); | ||
|
||
using (var peStream = new MemoryStream()) | ||
using (var pdbStream = scriptOptions.EmitDebugInformation ? new MemoryStream() : null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pdbStream [](start = 23, length = 9)
Let's call this variable pdbStreamOpt as it can be null #Resolved
Great start, thanks for taking initiative on this! I have commented on the changes. In addition I'd like to see more tests that validate the presence of debug information. I think we could use StackTrace API to see whether there is debug info or not for the compiled script. |
Thanks a lot for the review - will apply the changes 👍 |
I've applied the code changes requested and simplified the API - as you suggested, instead of passing in I also added unit tests to verify the presence of PDB (check for line number in the stack trace). PS. I also noticed this PR, aside from the 2 originally mentioned issues, also fixes #13482 |
src/Scripting/Core/ScriptOptions.cs
Outdated
/// <summary> | ||
/// Creates a new <see cref="ScriptOptions"/> with debugging information enabled. | ||
/// </summary> | ||
public ScriptOptions WithDebugInformation() => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithDebugInformation [](start = 29, length = 20)
To follow the With- pattern this should be WithEmitDebugInformation(bool value)
src/Scripting/Core/ScriptOptions.cs
Outdated
@@ -123,6 +134,8 @@ private ScriptOptions(ScriptOptions other) | |||
metadataResolver: other.MetadataResolver, | |||
sourceResolver: other.SourceResolver) | |||
{ | |||
EmitDebugInformation = other.EmitDebugInformation; | |||
FileEncoding = other.FileEncoding; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add corresponding parameters to the constructor instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/Scripting/Core/ScriptOptions.cs
Outdated
/// Specifies the encoding to be used when debugging scripts loaded from a file, or that will be saved to a file for debugging purposes. | ||
/// </summary> | ||
public Encoding FileEncoding { get; private set; } = Encoding.UTF8; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having default encoding might cause confusion. Even if the file uses UTF8 encoding there are still two kinds of encoding - with BOM and without. If it differs the script won't be debuggable. I'd rather require the user to explicitly specify encoding than to run into these subtleties with the default value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok will change to null
(that would be part of ScriptOptions.Default
too)
catch (Exception ex) | ||
{ | ||
// line information is only available when PDBs have been emitted | ||
Assert.DoesNotContain("at Submission#0.<<Initialize>>d__0.MoveNext() in :line 1", ex.StackTrace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:line 1 [](start = 88, length = 7)
This is odd, why do we have line number info here?
It would also be good to set the FileName and validate it's included in the stack frame string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was intended to be a negative test (does not contain the line number info). I will change it to a specific stack trace without line number, it will be much more explicit
src/Scripting/Core/ScriptBuilder.cs
Outdated
@@ -66,7 +66,7 @@ public int GenerateSubmissionId(out string assemblyName, out string typeName) | |||
} | |||
|
|||
/// <exception cref="CompilationErrorException">Compilation has errors.</exception> | |||
internal Func<object[], Task<T>> CreateExecutor<T>(ScriptCompiler compiler, Compilation compilation, CancellationToken cancellationToken) | |||
internal Func<object[], Task<T>> CreateExecutor<T>(ScriptCompiler compiler, Compilation compilation, ScriptOptions scriptOptions, CancellationToken cancellationToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ScriptOptions scriptOption [](start = 109, length = 26)
Let's just pass bool emitDebugInformation
src/Scripting/Core/ScriptOptions.cs
Outdated
/// Creates a new <see cref="ScriptOptions"/> with specified <see cref="FileEncoding"/>. | ||
/// </summary> | ||
public ScriptOptions WithFileEncoding(Encoding encoding) => | ||
new ScriptOptions(this) { FileEncoding = encoding }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new ScriptOptions(this) [](start = 12, length = 23)
Check if FileEncoding == encoding to avoid creating new instance if the values are equal. See WithMetadataResolver
{ | ||
// line information is only available when PDBs have been emitted | ||
Assert.DoesNotContain("at Submission#0.<<Initialize>>d__0.MoveNext() in :line 1", ex.StackTrace); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test where FileEncoding is null. We should test all 4 combinations of FileEncoding is null/non-null and EmitDebugInformation == true/false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@tmat thanks for quick responses, much appreciated |
6ad17bd
to
0b781da
Compare
95a3aae
to
668042a
Compare
sourceResolver: sourceResolver); | ||
sourceResolver: sourceResolver, | ||
emitDebugInformation: false, | ||
fileEncoding: null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't need to be fixed now but shouldn't the command line runner (csi) actually use this feature, so that we have better exception stack traces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes that's what I thought as well - let's add it in a separate PR
@tmat can you take a look again? applied the feedback + improved the tests |
/// <summary> | ||
/// Creates a new <see cref="ScriptOptions"/> with specified <see cref="FileEncoding"/>. | ||
/// </summary> | ||
public ScriptOptions WithFileEncoding(Encoding encoding) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithFileEncoding [](start = 29, length = 16)
Just realized passing the encoding in options is not enough. The problem is that encodings are not 1:1 mappings in general. Say you have a script file encoded by encoding E saved on disk. You read it to a string using decoder for E and then create a Script from that string and also pass in encoding E. Then to calculate the checksum for the source file that's stored in the PDB, we need to convert the string back to bytes using encoder for E. The problem is that for some encodings the resulting bytes might not be exactly the same that the file contains, unless the encoding is 1:1 mapping.
Also, the compiler implements somewhat sophisticated encoding detection. So it would be unfortunate if the user needed to reimplement that.
I'd suggest that we add overloads to core Scripting methods that take string code
today. We don't need to add overloads to all methods since most of them are just convenience APIs that call the core ones.
I'm thinking these 3:
CSharpScript.Create(Stream code, ...)
Script.ContinueWith(Stream code, ...)
Script.ContinueWith<T>(Stream code, ...)
Then the implementation of these methods would create SourceText using From(Stream, Encoding) method. Then we would pass SourceText around internally, instead creating it all the way down in the builder.
Could you give it a try?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, so the suggestion - if I understand correctly - is to rely on letting encoding be detected by SourceText.From(stream)
because internally it would try to do it by looking at BOM etc when loading the file content?
When we add the stream overloads, do you also remove manual encoding setting from options and specify that using the non-stream scripting methods will not allow debugging (since under the hood it will use non-debuggable SourceText
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we keep the FileEncoding
property. If the encoding is not passed to SourceText the detection is attempted (using BOM) and if we can't detect we default to UTF8 with no BOM. If there is no BOM the encoding needs to be specified, which is what FileEncoding would be used for. You can look at the implementation of http://source.roslyn.io/#Microsoft.CodeAnalysis/Text/SourceText.cs,3687bd0c8922a81f to see what it's doing. Note the exceptions it may throw. We should pass them on the to the caller of the script APIs that take Stream and document them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK sounds good.
One thing I noticed straight away though is that adding the overloaded methods with Stream
, will cause the analyzer to complain: Symbol Create<T> violates the backcompat requirement: 'Do not add multiple overloads with optional parameters'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What analyzer is that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not line up with how we've been approaching our APIs. There should be a single overload with optional parameters. That is part of the versioning story.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are multiple suppressions of this error already
That's not good. When did these pop up? Never went over any of these during API reviews. If this is happening we need to start flagging them and reviewing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rule is overly conservative. We can version these overloads methods just fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I filed an issue here: https://github.com/dotnet/roslyn/issues/16564
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rule is overly conservative. We can version these overloads methods just fine.
That may be true. It's also not a decision we can not be making in a PR in a release branch during lock down w/o discussion with the API owners.
I pushed some changes - I'm not sure how much tests there should be, given that it all ends up calling into the same underlying stuff, the stream methods are merely shortcuts. |
improve tests by using StackTrace API and diagnostic verifier
@tmat thanks a lot! |
4de580c
to
931fdf9
Compare
It seems that github is not doing the rebase right. The PR now has 300+ commits :( |
should we repoint the PR to the post-dev15 branch? at the moment we are targeting a merge to master |
The rebase looks correct. Problem is the PR is still targeting master as the base. It needs to move to post-dev15 as well. |
Modified the target branch to be correct. Back down to a |
src/Scripting/CSharp/CSharpScript.cs
Outdated
@@ -23,7 +26,23 @@ public static class CSharpScript | |||
/// <typeparam name="T">The return type of the script</typeparam> | |||
public static Script<T> Create<T>(string code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null) | |||
{ | |||
return Script.CreateInitialScript<T>(CSharpScriptCompiler.Instance, code, options, globalsType, assemblyLoader); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return [](start = 12, length = 6)
throw if code == null here as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and add doc comment that this method throws ArgNullEx
In reply to: 99941131 [](ancestors = 99941131)
Looks good, other than one last minor thing here: #16489 (diff) :) |
thanks - should be all good now 🙈 |
@filipw Thanks a lot for making it happen! |
let's enable this for CSI out of the box 😄 |
Customer scenario
As a user of scripting APIs you'd like to be able to consume the symbols to step through the script source. This is not a mainstream scenario, but very useful for script runner authors as they can offer debugging experience.
This was discussed with @tmat and @ManishJayaswal and the suggestion was to open a PR.
Bugs this fixes:
Fixes #12046
Fixes #1543
Workarounds, if any
Currently this can be achieved with lots of reflection against the Roslyn APIs and manually emitting the compilation. See example here https://github.com/filipw/dotnet-script/blob/master/src/Dotnet.Script.Core/DebugScriptRunner.cs#L25
However, this is hardly ideal.
Risk
Low. Some new public members on
ScriptOptions
Performance impact
Low. The original execution path is unaffected.
Is this a regression from a previous update?
No.
Root cause analysis:
At the moment Scripting APIs completely ignores PDBs and passes
null
into "pdb symbols" when invokingCompilation.Emit
.How was the bug found?
Lots of community requests for supporting debugging in script runners.
Usage & Notes
The new setting on the
ScriptOptions
allows the user to specify if debug information should be emitted and whatDebugInformationFormat
to use. If so, then when compilation is emitted, symbols are created and loaded.Additionally, the(removed this as it created some regression on the available stacktrace in case of errors)EmitPdb
setting is used to controloptimizationLevel
on the script compilation.In order for debugging to work, encoding is mandatory so I also added a possibility to specify encoding via
ScriptOptions
with the default beingUTF8
.The new settings (encoding + emit pdb) have not been added to the
ScriptOptions
constructor because the defaults (UTF8
+false
) seem reasonable to me.