Go back

What's so great about R8?

66m 29s

What's so great about R8?

R8 is a whole-program optimizing compiler for Android, used in release builds to remove dead code and apply advanced optimizations like inlining and method merging, which improve app performance and prepare code for efficient on-device compilation by ART. In contrast, D8 serves as a fast, incremental debug compiler for development. R8's static analysis can be challenged by reflection or JNI, requiring "Keep Rules" to preserve necessary code, though overly broad rules from libraries may limit optimizations. The compiler also strategically organizes code into DEX files, prioritizing startup code and maintaining package locality to enhance memory performance. While many apps use R8, its adoption could increase with better tooling and awareness of its benefits for performance and size reduction.

Transcription

13099 Words, 70138 Characters

English
[MUSIC] >> Hello and welcome to Android Developers Backstage. I'm Tori Norby from the Android Tools and Libraries team. >> I'm Romanghi. >> I'm Son from the R8 team. >> I'm Chris from the Devax Performance team. >> I'm Shai, I run Android Performance. >> All right. Today we're going to talk about R8 and its implication for performance. >> What is R8? >> Yes. I guess that the people who have been following this podcast might have heard both Romang, Tor and Shai mention R8 in a number of episodes. Yes, it's very great to have the opportunity to talk about R8. So thank you very much for having me. First of all, I should probably clarify that we are doing two compilers. We are doing D8 and we are doing R8 on my team. They're both responsible for taking your code. The last mile to be able to run on the Android device. So when app code is written in Java or written in Kotlin, then it's compiled by the Java compiler, the Kotlin compiler down to Java byte code. But the Java byte code does not run on the Android phone. So it has to go through another compiler, which is then D8 or R8. And D8 is the compiler that you use for your day-to-day development. That's the debug compiler that basically runs fast. It's incremental. You can do your edit, run the debug step, run on device simulator very fast. Then R8 is the fully optimizing compiler that you want to run when you have to do your final release build. So R8 will apply all kinds of optimization to your code. The most important one, I guess, is removing completely dead code. When you build an app today where you use tons of libraries. And when you use tons of libraries, there's a lot of code users and use use, like one or two methods in some libraries, and then there's a lot of dead code. So what R8 does is that it is a full or whole program optimizing compiler, which means that it compiles the whole app in one go. So it has full knowledge of everything that's happening, which means that they can actually go and see statically at compile time what code will never be used at runtime. And that's kind of the primary optimization that is removing completely dead code. And that why do you have two compilers? So why we have both R and D8? Because there's two very different situations right. You want to be able to develop your app. Then you want to have a very quick, smooth cycle when you edit your code. You want to run it on device or emulator. And that has to take like a few seconds, which means there's no time to do any optimization. You just want to incrementally compile the single part you just changed, adapt your APK here, whatever, and then deploy to the phone. Where's the release app? Is the one that you want to spend maybe 10, 20 minutes on optimizing? And that's why we have the second compiler, which basically runs, compiles all code. Where's the debug compiler just compiled the bits that you've changed? And therefore it cannot do any like whole program analysis. This is not time for it. I see. So everyone uses D8 out of the box. Like it's difficult not to use D8. So you can't do an Android app without D8. So everyone who uses Studio in Gradle are using D8? Yes. Are they all using R8? There's at least a more than half of apps are using R8, maybe up to like 70%. Our current analysis says so there's quite a few that use R8, but there's definitely more that should because-- I didn't know if you're using it or not. How do you know whether you can go into your built-up Gradle file, built-up Gradle.kts file, and then you can check there is a setting on your release build, whether you are using optimized-- using optimizing compiler or not. And it is minified enabled. Yes. It's currently minified enabled. We are happy just to say that we're also doing a new DSL, which is also already available on the flag in ATP9, so that you have a more intuitive way of actually turning on optimization is as optimization equal true. Which is-- Why wouldn't people just do that? Well, there's a number of reasons. So as I just mentioned, the R8 does whole program optimization, which means that it's static near analyzing the code, which means that it's trying to compile time to figure out what's happening at runtime. This is all good when you're using kind of normal code. You're just calling function, calling methods, doing whatever. However, Java has a reflection, which means that you can add runtime. You can conjure up strings, and then you can just look up classes on methods of fields, using string names, which is something that typically at compile time, you cannot see what the compiler-- sorry, what the program is doing. Which means that you're conjuring up a class name, and then we can see that compile time, then, well, it's removed. It doesn't work at runtime. Class not found the exception, which is not what developers want. So that is when you have this kind of code, you can't just-- And you have the same point with JNI, right? When you're in Java or cutting code from JNI. Even if you're not using Reflection yourself, a library could be. Absolutely. And it's usually libraries that do a lot of reflection, because there's a lot of libraries that implement generic type of functionality, which of course is sometimes easiest to do with reflection, right? You can just look up what's on your classes, and you can do something in your code based on that. So that's where the optimization breakdown, and we removed stuff that we shouldn't. And this is-- so from the indirect side, ever since the early days when we were the support library, we've always had policies against using Reflection and using JNI upcalls whenever you can avoid it. For this reason, I mean, there's also the other reason that they're also very slow, which is another great reason to avoid them. But we know that we are going to get into a murky territory with our aid if we ever use Reflection unless we're very careful, unless you work around it with Keep Rules. Yes. Yes. And you mentioned the MathiWood Keep Rules, which is basically what you need when you have a program that use Reflection, then you need to tell our aid that, well, there's something here that you can't see at compile time, but I am at runtime doing something on these classes, these methods, these fields. So you'll tell our aid as the developer through these Keep Rules that you cannot just remove these fields. You have to keep them, even though the starting analysis of a program doesn't show that they are used. And it's not just at the program level, right? When you are the author of a library, you can write Keep Rules specifically for your library, and then the tool chain will merge all of those so that you keep everything that you should keep in your program. These are called consumer rules, and yeah, those are merged into the whole program when that library is used. So to your point, you already are a crazy like Android X can use Reflection and JNI just needs to provide the right Keep Rules. Yes. A classic example that I always give is WorkManager. So WorkManager will instantiate workers based upon string names, especially when it's loading those up from say a database after the fact and trying to like check in with the system about those like the string names need to be consistent across builds and continue working. So what WorkManager does is it has a Keep Rule that says, hey, Keep Classes that keep these properties of these classes that extend to this class. And it is tightly scopes to ensure that all that it's doing is preventing optimization of those specific worker classes. Maybe you guys say more because I feel like you ran into something where these rules were overly broad. And then say you end up consuming someone's well-intended poor Keep Rules. And as a result, all right, couldn't do its job properly. Yeah. So it really depends on the individual library that you're looking at. I did a lot of review over the Android X libraries and there's very limited use of broad Keep Rules. Like I think the main cases that you'll see inside of Android X is things like JNI upcalls into known Pojo structures to construct things. So you use a Keep Rule that says, hey, R8, don't optimize anything in this very limited specific package. Now that's reasonable. It's not terrible. But what is pretty bad is when libraries just say, oh, I have some sort of R8 problem. I'm just going to keep my entire library. And this is unfortunately, as we've been talking with people trying to help apps improve their R8 configuration quality. We see a lot of people sharing their configuration.txt with us. We review it and we see, oh my goodness, this library is keeping everything. We look at these libraries on Maven. And it does downstream effects, right? Because when you keep something, at least in my experience, there are other optimizations that I can do for performance that can't happen because you told it to keep everything. Absolutely. So R8 will do, I mean, the main three things that we talk about R8 doing is, as Sorrow mentioned originally, dropping dead code, tree shaking. It also will optimize. And like this is kind of kind of critical because it'll say, hey, this method is always called with this parameter. Let me inline that parameter. Or this method is called all over the place, but it's tiny. Let me just inline it. And I think it's worth seeing that some of the optimizations are, it can do, you cannot do yourself by hand, right? Because it works at the bytecode level. So there are things where you just detect, for example, that there are no subclasses of this open method. Therefore, we don't have to make a virtual dispatch. Is that the kind of thing? Yeah. That's wonderful. It's also like, if you have an interface, you only have one implementation, right? Usually, you have an interface to be able to have multiple implementations. Sometimes you have a mark in your test, but when you actually build your app, you don't use that implementation, which means that R8 can see that this interface only had one implementing class. And then you could just merge the interface down into the, into the, into the class, implementing class. And we could also do, and that's where a, a keep rule would keep a class that suddenly made it no longer not implemented. And therefore, the optimization can happen. Exactly. And if you keep the interface, then you'll say, well, then you're telling R8 that might come other implementations along that we can't see statically. So that will kind of prevent that specific optimization. So the impact of these optimizations is actually much larger than most developers realize. And this is maybe a really interesting sign-out. We said there are two compilers, I think that's a growth understatement of how many compilers are actually involved in this. So we're going to just enjoy the-- - To Dex likes compilers. - That's right, there's many, and one of the many other compilers that are involved in this process is the compiler that ultimately runs on the device, and then takes the bytecode that was hopefully very thoroughly optimized through Sorns, maximum power, whole program optimizer, and then compiles that to actual instructions that deceive you that you have in your device. - Yeah, I'm glad you bring that up because to me that's key, one of the things I found with RIT is that the work does prepare-- like paves the road for art and I've seen, yeah, it can make a major mess up the first. - Exactly, so we co-optimize those different compilers so that the compiler that you run on your computer as the out developer to build the code can not only optimize the code as much as possible, but leave all kinds of hints so that then the compiler that runs on the user's device can take it further. - Can you give it a example of something that you would leave behind in the Dex code that helps the Dex code, I guess? - Yes, absolutely. So one example for that is a lot of after-veilers use include baseline profile, startup profiles. So by doing that, and it's own topic, and we could get into that another time, but by doing that you basically say, hey, these methods are more important than others. All methods are important, but some are more important. These methods are hot, these methods happen to start up, they need to be fast. And so one thing that we can do is we can say, okay, if you have hot code and you have cold code, that is a hint to inlining. Inlining happens when you have code that calls other code and you can just say, why don't you take the code that is being called and pull it up to the caller? Well, should you do that or not, is sort of guesswork for the compiler because the compiler doesn't get to run the code, it just compiles it from one form to another. But if you give it a profile, you say, hey, there's a hint, these methods are hot, these methods are not so hot. And then the compiler can say, well, I don't want inline cold code into hot code because then it becomes look warm. - To be fair, that isn't actually from R8 either, right? Like that was came from a run. Are there examples of cases where there's stuff in the Dex code, in the, I guess, byte code that R8 saw that thought, like, hey, I'm going to inline this, but I'm going to leave a hint about what it is. I know that earlier, we did resource optimization. I'm sorry, resource shrinking, and we would note which are constants actually corresponded to original resources, even though it was just a number in the end. That was a useful piece of information later in the pipeline. So I'm just wondering, are there cases of code optimizations happening are at the R8 level? That is. - Yes. So for instance, there are certain patterns in Java or Kotlin programming language that are a very pretty syntax nice to use. And we end up deshugaring those patterns to something that they underline Java language byte code or what have you can actually work with. But we can deshugure the beautiful code into functional code and then in such a way that they compiler that then lowers that functional code to ugly, non-human readable code for the machine can then say, oh, I recognize this pattern. This is not just any other code. This is a we're appending to strings, something that happens very commonly in in programs. I have just the right super specialized intrinsic for that that pads the bytes, the right way, and alchids the memory in so and so and takes advantage of all kinds of properties that really that compiler can do. So when we test these development test tools, we develop and test them not in isolation but in cooperation. We have different teams that work on this compiler. And that compiler they meet regularly. We run complete benchmarks that test the whole thing end to end. And we sometimes we go down to the individual instructions or performance counters for the CPU to see the effect. - I think we had an episode with you and remember talking specifically about this. So if you're interested in this, there's an episode on that. What was the compiler explorer and some really so. - Yeah, really so. Yes, we added a couple of years ago support for two compiler explorer, godbolt.org. You can run d8, you can run r8, and you can run decks2out, which is the on device compiler, and you can see everything. - So the only thing when you use something like compiler explorer, because r8 is a whole program optimizer, when you're not testing against a real application, you have to be extremely careful, because sometimes r8 will see optimizations that make sense in that limited context, but who not happen in a larger application. So I found sometimes, once I was forcing r8 to keep some things knowing that it would be kept in the in the real app so that I could see the actual optimisation that would benefit from or not. - Is this something where I mean we talked about the startup profiles and so on. I'm wondering if there's more profile capture that would help r8. In the old days, you had these things where you would run a collection run with a compiler, and then you would feed it back later for proper optimisation. But I'm just wondering, is that something that you have or are working on? - So definitely the r8 is well aware of baseline profiles and startup profiles. So basically what we do as Shower already mentioned, when we know about profiles, we just make sure we don't pollute one type of code with the other type of code so that we can make sure that what you have in your startup code is not being polluted by non-starred up code, what you have on your baseline profiles or the hot path which is after startup is not polluted by non-starred up code. So that we have these kind of free categories. There's definitely more opportunities for r8 to get more information from from profiles. It's something that we've been working on, but we have this is kind of the stage we are at the moment. - Speaking of that, I'm going to ask a random question now. But you know, every now and then when I analyse my APK, there's like six or seven or eight dot Dex files in there. And I'm on a hunt to find my code. What is the logic to what ends up in which, is this something that r8 is involved with? There is this AGP or who decides what goes where? - So that is exactly one of the parts of both r8 and d8 that is laying out the final Dex file. So it is r8 that will do the final distribution into files. And so we have a rule that says that startup code goes into the first Dex file. If it fits, hopefully it does. Otherwise, it's probably your startup profile is to broad, but otherwise it will flow over flow into the next Dex file. And after that, we'll then distribute the rest. And the distribution is trying to keep code locality, which means that we'll collect all the classes in the same package in one Dex file. And so that all classes in some packages in the same Dex file because code locality is actually pretty important because code within the same package will typically call code other code in the same package, which means that if that is altogether, then it's on the same memory pages and that will kind of make going from one function to another way faster. And it's also that when you have then run Dex to out. So the compile that produces native code, it will, to most extent, it will keep the same locality as the Dex file has, which means that the layout of the actual final, if you have machine code for your method, then they will have the same locality. I mean, this is probably just an APK analyzer usability thing, but like I'm always picking the wrong one. And I get excited because I expand it and I see my package. It's one of those things where like I'm finding all the Android X libraries and like it seems like my code is lost. So I don't know if you could have like an index or something package to file name index as a text file at the top. Well, you can click and shift select multiple Dex files and load all of them off at the same time. But I want it, I'm drilling it, I'm trying to get, I want to look at, I want to decompile the code and maybe I should just use romance tool instead. Right. I started to try to explore. Yeah. So, I mean, so you do that, right? If you click all the Dex files, then you can find, and you are looking at the deobviscated names, which is easier to do when you're using a bundle instead of an APK because it has the mapping file embedded inside of it, then you can just find your package click through and you should be able to decompile it from there. I usually use backsmiles to just deco file everything into it. This is simply and there's grabbing it, that's kind of the fastest thing because you're always hunting the wrong place, that's what you're just doing. Just a good young code. Yeah. Why not? And when you do, you see very interesting things. So, soren was down as for instance about how sometimes you have an interface in your program, and there could be different classes that implement this interface, otherwise you wouldn't write an interface, but actually in your release build, in production, you only have one class that implements that interface. So, if you write your code in Java or Kotlin and you say, "Oh, I have an iPhone. I don't know if it's a real foo or a foo for test." So, I call iPhone dot some method. And at runtime, this is called a virtual info, where you have to say, "Okay, this is an iPhone. Let's look up some tables, see what kind of, what is the actual implementation, where does it come from, and chase a pointer, chase a pointer, chase a pointer, finally we can call the code." And so, in your code, I hope you're writing tests. And as you're writing tests, maybe you extract that interface, you have a test double, you call that and so on. But this magical whole program optimization that Soren tells us about can see, "Oh, in your production APK, you don't ship the tests. I hope you don't ship the test to the user." So, foo, real foo, and I can get merged. So there's no need for an interface anymore. And then the instructions lose one level of interaction. So there's one fewer pointer you have to look up and then go to a different memory address and load from that. And it turns out that really helps the cell can get things moving because every time you load something from memory you might stall. That might not be in the cache. You might need to go to the DRAM and suddenly a hundred cycles you're just. And these kind of teamizations can get very impressive. I wrote a blog post about something that we're working on for compose. And basically we are building objects that it involved a lot of like bit manipulation. And so when you compile it, you know, the regular way, you get a stream of like 200, 300 instructions, like a couple pages on your screen. And then you turn on our right and becomes loading one constant. Because there's enough in mining that has happened in our right and then art is able to see more of the program and can. Compress everything down to just the one instruction that was needed. And this is the part that I'm, you know, I think it's important to make APK smaller and so on. But you know, at least on my phone I have enough space. I have a good Wi-Fi. You know, like I'm less motivated by the shrinking. Although I understand that the shrinking does offer, you know, further performance benefits. But I'm most excited about making it run faster. And again, it's it does a lot of optimizations. You cannot do yourself. You know, because that's also sure you can be in line by hand, right? But there are things that you know, you won't be able to do on your own, especially when it's library code. So and it's not every dead that you're handy. The tool that can give you what double digit percent performance improvements kind of for free. Yeah, I think that that's a really important part about like taking these like, hey, we're optimizing things and like kind of making that into a concrete number. Now I don't have a concrete like number. But like one of the things that I think is always impressive is when we reach out to partners and they tell us their stories about turning on baseline profiles and R8 at the same time. And you have lots of people reporting 30% performance improvement. And also one of the one of the big things that was surprising to me is just how much it improves A and R's. And I mean, to some degree that's kind of intuitive, right? Like the worst performing case of my app when it actually A and R's is probably going to be code that I. Oh, sure. So A and R application not responding is when your applications main thread isn't able to respond to messages for a period of five seconds. That's a huge amount of time. A lot of times it happens in the background when some service is doing something. It could be any number of things that are happening wrong there. But one of the commonalities to a lot of it is just that you're hitting infrequently used code that hasn't been optimized. And by turning on R8, a lot of those A and R's get to be pushed down below that five second threshold, especially like these background A and R's, which, you know, are unfortunate for you crashing your application. Maybe they're not super visible to the user, but you definitely want to avoid them. It's awesome to see how much those A and R rates go down when people turn on our rate. Now we'd also add that I think is critical when you use cutling and compose because with cutling, the standard library becomes a library inside your application. So so when you mention that, you know, you're going to get rid of the code that gets on use. And obviously you're not going to use the whole standard library. So it's an easy way of like, well, doesn't it also Kotlin adds and then yeah, lambdas. No, I was going to say something different, but that too. Okay, I was going to say inserts a million boundary checks anytime you jump between Java and Kotlin, it'll do some null checks because they don't know. Don't get me started on that. But you know, you hope for a better analysis. So from hope, program analysis, of course, we know when there's a none null, well, value always flowing in, we can remove a null check. But we have actually done for the latest version of HTTP. We have gone even further. We have actually had an option to just take out the Kotlin null checks, either in the very, the hardware, just remove them or just replace them with one instruction that just will give an NPE, but without all the text because these null checks have like nice parameter names. Yes, that's nice message. So you can see which parameters of your methods was none null, which is of course nice when you're debugging, but in the release app, that should not really be interesting. You might need the null check, but you can get it for almost for free, or you can just complete remove it because it's almost never needed. And those checks are very frustrating because also the way they're implemented in Kotlin is pretty expensive. Yeah, there are giant strings and string formatting and stuff. And like, not just the string formatting, it's like those check in require calls, like the way they work, like the throwing of the exception is in nine of the call sites, you page in a lot of code that doesn't need to be there. Yeah, yeah. And that just calling a dot get class on the reference. That's kind of very cheap. It is remarkable how much code in apps is for handling errors. That never happened that well, ideally never happened or when they do, then that code is necessary. Well, I said that never happened because some of those null checks, you know, when you come up with a code, right, because the compiler assume that Java code my code into Kotlin. So it has to double check even though you said this type is not nullable. So, you know, if only you Scott and you know, it cannot happen. Or do there's a component bug? So this is really one of the things that I love about these tools. So in my in my current role on performance, I, I just want everything to be fast. And when developers bring their fancy new tools and libraries and languages, I just think of all the ways that this will make the apps slower. But I also worked on apps. And as an up developer, I just want the best tools and I want to write type up, suit a code and it becomes whatever logic. And finding nulls for developers actually does save them time. That's sure. Like this was a useful productivity feature, just a bad performance feature. And that's where yeah. So this is a great example. We can get to both, the best of both worlds. So you can write a beautiful code, nice and clean, add your tests to all your abstractions. And in your development environment, you get all the benefit of that, all the safety, all the testing, all the readability. And then when you're ready to ship, you press the button, go make a coffee, all right, it's done. And you have all of that, all of those safety checks that are actually provably unnecessary go away. Those huge libraries with all the robust functionality that you are depending on the functionality that you don't need goes away. And you ship something optimized to the user when you're ready to ship. Yeah. Sir, and you said 20 to 30 minutes. That's more than one coffee, I would say. Yeah, well, that depends on what coffee you drink. Yeah. Yeah. I think we talked about this is all good, right? Like, let's talk. There are some downsides, right? So we mentioned one is that gap control. Go tell that to people. No, I, you know, because I think I believe you have been working on some mitigation. So I want to talk about the, you know, the potential onset. One is your app crashes. Another one is, you know, when you, there's a big discrepancy between what you're debugging testing in your Android studio and the crash report you get, right? With a line number, so much and so on. So can we talk a bit about like what the, what these things are and what you're doing about them? Yeah. Yeah. So first of all, of course, when you're doing optimization and you're also one of the optimization we do for making things smaller, we rename things to APC and we merge classes. So suddenly you have one class at represent 10 classes. So when you get a crash and you get a stack trace and you just get a B C on your call stack, which is not very useful. The good thing is that our rate outputs a mapping file, we can actually use to map that thing back to where you came from. And one of the things we've added recently is that the inside those stack traces, the stack list with the very confusing names, we have embedded the hash of the concrete mapping file that you need to uncover the stack trace. Because as a developer, it's very hard to keep track of all these things. But then we have also added into studio so that when you have a testing this 3dsab in studio, then you can just click on the button in the lock cat viewer and then it will go fetch the correct mapping file from your build and it will recover the stack trace. You don't have to do manual command, you can just use it in the ID. Yeah. So stepping back a little bit, like we've had a lot of this stuff kind of working by default in production, if you're using crash litix and you're uploading a bundle file and it contains a mapping file, all of that would work kind of flawlessly. And now the big changes, I think it's like requires a GP 8.12 or something like that. But once you have that, now that can work in studio too. And we're trying to make, we're trying to make developing on an R8 application feel just as easy as developing a release D8 build. So if I'm switching to release binary locally and I hit run and then it crashes and I look at log cat. If I click on something there, are we automatically deobviscating it in place in log, okay, even better. Yeah, I think we deobviscated in place and then we have a button to see the original stack traces at how it works. Yeah. You can still get the Mac on one if you maybe you want to check whether it's the same as you got from the wild. Yeah. So that's a huge improvement. Are there cases where I can turn on optimizations that are unsafe? Yeah. So we, so actually this is maybe a good step into the fact that we have done this massive documentation overhaul for R8 over the past year. We have for a long time, we had a lot of little details about how R8 works and integrates with AGP, but really didn't describe its user interface in our documentation. And now we have a page on that. We have a page on documentation for additional optimizations that you can force on that may not be safe. If you're, if they're misused, but you know, they, there are a number of those you could force, you could force assume value, you can use additional key peroles that kind of serve as the opposite. Instead of don't optimize this, no, I can tell you ahead of time it's safe to optimize this or you can force this optimization. So you could assume values to say that this field or this constant, this variable is always constant. Assume that my debug flag is always false. You can do things like that. There are also top level flags also that we document. and the global flag ducks that talk about that too. But just in general, we know that for a long time, it has been very painful to learn how do these key rules work? What is the syntax of them? Is this an appropriate one? And we have documentation talking through what are good patterns, what are bad patterns? Why should you be using these and lots of examples? - That's fantastic news, 'cause I have spent a lot of time reading through the source code of our aid to understand some of the rules and stuff. - Never again, hopefully. I mean, it's interesting, but you know. - Well, I'm never again for the average person. I'm sure you'll be looking. - Yeah, so first of all, there's, I already had a lot of optimization and there's no way of controlling which one run and which one does not run, because that will just be way too complicated. - Well, you're only picking the safe ones. I'm assuming. - Yeah, yeah, of course we are. And then what has been a problem, especially in the past, is there's this nice knob that says don't optimize, which turns off all of the conversations because then you can probably fix some crashes that you had and then everybody forgets about it. And that is a real bad smell and you should never do that. We have done various things in both ADP and Lint to make sure that we call out when people do this. Maybe they don't know. They just forgot or they came from somewhere they ended up aware of. So we helped them with that. - This is something. So if I'm, you said ADP, so is it like a build time warning? - Yeah, so for a long time, we had two different configuration files that were considered default pro guard flag, or the default flags that would be passed in, that would be things like keeping things that we know that the framework we're calling to that you might not need. I think that includes the default like enum key rules. But one of the things that we recognize, as we were starting to play with like, what are the defaults, what does it look like when I get things out of the box from Android Studio, as we just noticed, oh, you know, this thing that we've always had as a configuration flag, which a lot of people have in their build files is get default pro guard file, and then you pass in a string. And that's either Android optimized. Or Android, what is it? Android. - I can get the history on that, right? Because this actually comes back from even from the Eclipse days. - Yeah, right. - And we had one of those things where we go like, oops, that flag's bad. But people had already copied those files into your project. So we thought you can solve every problem with one level of interaction. So we thought like if we just let you point to a file, it will maintain the file for you. That's why we did that. - Okay, yeah. - But obviously, we never looked at that again. - Yeah, and at the time, I think it made sense that turning on optimization was a potentially dangerous thing because it wasn't super well tested. And now, you know, there've been what, six years of our eight, like it is very well tested, what optimization does and can do. And so it does not make sense to support that as like kind of a default configuration out of the box. So what we did is we added a lint rule to warn you, hey, by the way, you're using a flag that is, so I mean, if you're declaring dashed don't optimize in your key-pural file, that's pretty obvious that it's doing something rather dangerous. But what isn't obvious is the AGP API that is give me the default pro-guard rules, right? So what we did is we added a lint to say, hey, don't use this one, use this other one. It doesn't include don't optimize. And then in AGP9, we just broke it. We just said, you cannot do this. If you want don't optimize, you have to tell it to me. You know, don't be shy about it. Put it in your key-pural file yourself and admit to your failure. But I think there are a lot of these kind of usability problems that we've been looking at recently, especially in AGP configuration of R8, because there are a lot of things that you can kind of control in ways that are not obvious. So one of those is the default pro-guard rules themselves. So something that I found out that was very surprising is if you do specify a custom rule file, a custom key-pural file, you don't get the default rules. You have to add them manually. OK, fine. But if you delete that file, if you don't have a rule declaration file, then you do get them by default. It's-- we're working on improving the effect sense to me. And to anyone who has read all the source code, it makes sense. Yes, when you know how it works, it's going to surprise you. Exactly how it's implemented. Yeah. So we're working on improving these and kind of overhauling the experience for that. Yeah, and having more documentation is great because some of those rules, those key-purals can be a bit mysterious at times. And I know they were shared with other tools. And the documentation was not great. Yeah. And also when you look at the default key-pural, there is a good reason for them because even though we said reflection is not a good thing, then initially Android was built on reflection, right? The whole view UI model is based on reflection all the way from-- Not from. Well, we'd say Java in general. It's a very-- Yeah, it is kind of a first-- But I think it's been a shift in the community as well. We've seen especially now with Kotlin a lot more cogeneration, especially when it comes to dependence injection. So I assume it's probably easier now to-- Well, I'm assuming you have-- I know we have special handling for views.xml, right? Any layout file, it's all reflection. That's how it works. Yes, but when you get into the guts of a library, it's much harder. And I think this is a great segue to talk about G-Song. So G-Song is a popular reflection library. Sorry, a popular JSON inflation library from Google. But it is fundamentally built on reflection. It is very, very easy to build yourself a footgun in JSON, even if you were trying to use the annotations that they provide that do have key rules associated with them. And that talk of code generation is a great segue into the fact that there's just way better alternatives now. We have Kotlin X serialization. We have Moshe code gen. Both of them are totally fine and great alternatives. There's even guidance on the Moshe code gen page, I think, about migrating from JSON. But both of those work flawlessly with our rate out of the box in comparison to JSON, which will sometimes work. Now, I've seen legacy JSON code, I think, is a lot of what we talk to developers about in terms of why they have wider key rules. And it's very common to say, well, I don't want to have to migrate this around. So let me put a package-wide key rule on them. And if you're very diligent about saying, JSON will only inflate things from this particular package and it only understands that, then that could be fine. And you put the data classes-- Only the data classes in that package. Yes, but you have to be very careful about it. And code gen is just safe by default. And it's probably-- And also, even if you're diligent about it, I mean, over time, let's say you have a new NGR joining your team, they may not be aware, because that information is somewhere else entirely. It's in this file, like, mysterious file, somewhere in your project. And it's very easy to not know that it's there. Can you write a unit test that verifies shrinking behavior? Like, if I wanted to assert that in my release binary, I expect to say this and never want to see that. Is that something that developer can do today? Yes, I think we're not talking about why are you keeping so much as we're talking about-- It's a card against from an scenario. Like, I have now intentionally segmented my code over here. And I have the key rules just so. And I want to protect myself against someone doing something dumb later. Is there something I could express that? So there's a key rule. There's also what Shyman said is called check this card. Which basically says that what matches this rule should never be in the output. So that's a way of ensuring that something gets removed. It can be used for various purposes. Of course, make sure that your test code doesn't get into the app, because also make sure that if you're building different flavors like a dog food version or a beta version that you don't want features to leak. So we can also use that to make sure that your build actually is configured correctly so that it will remove what you expected to remove. So this is one way of making sure that you are code-- That's super interesting. Yeah, because I've often in the past use, like if false, and relying on the Java compiler removing byte code, that is like-- But either or no, you did that. You didn't really know that it was gone. So you didn't necessarily have a test that was looking at the-- Exactly. This was just like-- You believe. You believe. You believe. I verified one, sir, twice. And then I was like-- And then you got bored. And as the tools update out from underneath you, you just assume that that's true. I mean, it's normally a way better way to keep code around, because it's still being compiled. So if you comment it out, things happen. But this sounds like a way better thing to actually market as deletable. So this is where-- this is one of those cases where performance and security meet oftentimes they work against each other. But in this case, they come hand in hand. So Soran gave the example of, I want to make sure this feature doesn't ship because it's not ready yet. Or I want to make this sure that this code doesn't ship yet because it's a secret new project that we haven't announced yet. And it's in development in our code base. But we rely on R8 or other tools to strip it before it goes to the user, because we know how much bloggers like to-- nothing has ever been leaked by looking at this. It's a flag on the top. Is this just an enforcement flag? Or is it a way to say, hey, R8, slice this out? It is a way to check whether it's been moved, and the bill will fail if it's still there. So it's not forcing it out. We don't have a way to force things out. Well, that's your feature request for the other person. Well, so remember that the default behavior of R8 is force anything out. Like that is the default of shrinking. And as long as you're not disabling shrinking, you'll get that by default for every single class that doesn't reference. Yeah, that is why R8 takes time to run, because we don't tell you what to remove. Rather, we discover what are all the things that need to be kept. So we start with the routes, the entry points to the program, the code that we know is live by virtue of its definition, and we walk a graph of sorts, and we color it. And this is why I could write two lines of code and run R8, it would take the same amount of time. to re-automize the program as going from scratch because it is a whole program optimizer, which is why you don't want this in your edit build run cycles, but you do want it when you're ready to ship. Are there ways to make it incremental? No, not at the moment. No. So yeah, it's basically a whole problem. Yeah, it is a hard problem because when you change one thing, it just have a ripple effect with the whole compilation pipeline, so it's a hard problem. It is like if there's a graph, I'm assuming there are subgraphs, and I'm assuming that you could. But anyway, like I'm sure that's another request. Speaking of libraries, one of the things I'm excited about is the running all-rate on just libraries. Was it partial? I was going to ask about that. I was unsure. I'm trying to remember the name. Yeah, so gradual-arade is kind of what we describe it as, but the API is package-scope, and that's what the documentation calls it as well. So that's a way for you to say, you know what? I have some dangerous things, some dangerous reflections somewhere in my application. Let me just get the default configuration for something that is a subset of all of those packages. Let me only optimize a package at a time and kind of add a package at a time. Now, keep rules today will allow you to opt out individual packages by saying, keep my package.star star. Those are pretty dangerous. This one, because if you specify something too wide, you can turn off optimization. We think of those as things that you only do temporarily. This is kind of giving you the opposite tool to allow you to work incrementally and add more to your optimization as you are trying to adopt R8. So instead of doing a whole program analysis and optimization, it is just doing that optimization over a default list. For instance, I'll enjoy the x libraries because those have been just to say is that for baseline profiles, initially we only did it for composed. This is one of the ways we could ship compose because we wanted to pre-bake startup or baseline profiles into compose because we needed to run faster and we knew that part, you know, we can make we can mark hot even if there's also important application code. Initially we started with just known libraries and it feels like you've done all this work on the Android x libraries to make sure they are safe as well. And so I thought I heard that you've done something where even if people aren't using R8 in general, we can at least do it on that part. Is that the same thing you're talking about? Yeah, so that's what I'm talking about. But at the end of the day, the API that we ended up with was one where you still specify a list of libraries and in the documentation we say at the very beginning, put Android x, put Kotlin in there because those libraries are safe and those are some of the most important ones. But I would say not safe, but something like compose, it really helps to really do it. It really does. Yes, because every with component that you use the lambda, we at least want to merge all those lambdas. There's a lot of things. Not enabling our eight at least on compose or especially I've seen a couple of configuration.txts come by where people will disable it for all of compose or all of Android x because they're probably working around some weird little bug. Please, disco that. Please pull that down to the absolute tiniest version that you can because if you say I don't want an optimized toolkit, I don't know what kind of user experience you want. Yeah, yeah. And talking about compose, this is one of the libraries we have been spent quite a lot of time on the R.A. team to actually focus on because there's a romance. There's a lot of Kotlin magic in there with lambdas and and and nice and use of Kotlin features, but it also kind of generates a lot of code that is sometimes slow-ish. But we have really worked our way through that code and and done value propagation in all kind of source including handing bit sets and stuff of that to make sure that we can act to trim that code and make it fast. So I think that when we had the last work on that part, it removed the number of instructions in the in the in the composco by about 20. So that's one is interesting. Can you elaborate a little bit more on that bit set because that's something that you know composed the repairs will never see, but it is fundamental to compose in a very important optimizer. Yes. Yeah, yeah. So that is when you have a composable, it usually has like huge amount of things you can say. So if you have a text field, right, they have a caller, it has style, it has like 20 plus of various kinds. And then there's the way the compose layout in it does is it use runs is that it passes bit fields around to kind of check the state of what has changed, what has not changed. And when you do the right handing of these bit fields, then you can suddenly trim out a lot of code that is not needed at runtime. So there's a huge benefit there. Does I do the same for Cuiscaux, does the same for optional parameters where you end up with a bit field and the problems that you know, it gives you an extra function called lots of branches. Yeah. Yeah. We do the exact same for calling and default arguments. So if for instance, also if you could see that that bit in the default arguments field is never set because we kind of find all the calls that we say that this bit is never set means that this argument is never passed. So we will remove the argument completely. Another example of this is the dependency injection. So if you're well behaved up developer, you write your tests and you create these interactions dependency injection to get your dependencies from and swapped them out in testing or maybe not even in testing in different environments. That's an interaction. I need to go here and I need to get the thing that I actually need and then operate on it. And if all right, can prove that getting the thing will always, the outcome will always be the same. It will always have the same result. Then we can remove that interaction, peel away the abstraction. No, I'm sure we've talked about this in the previous episode about performance, but I found that it's always beneficial when you try to write code that's clear to both the human being and the compiler. Well, it's easier to read the maintenance code, but also you get a lot more optimizations out of everything because the, the compiler is kind of risen about your code. You can also do as a team. It really liberates you to do things where in the past, you might be worried about, oh, what is the performance impact on this, especially for users that don't benefit from a feature. And now you don't have to worry about that. You have to worry about that less or not at all. And composes a great example for a team that went from adopting R a to fully bind into R a to the extent that they develop thing, they write their code in ways that they could not write their code and still assume good performance because they tied in with R a, but it's not the only team, of course. Yeah, all right. Doesn't only emit decks, right? And the reason I'm asking is that Android Studio, we're increasingly using more and more compose. But we don't spit out. All right, we run on, on JVM. Right, could we adopt R a. Yes. So, so there's no, so as I said in the beginning, right, the D8 and R a is for taking your code the last mile to decks, but it is actually so that R a can also go class file to class file. So you can optimize any, the other byte code with R a actually when you get R a on as part of your build with ADP, R a has been optimized by itself. So if you actually are so unfortunate to get a crash in a GP in an R a compile, you can actually see that the R a part of the stack trace is, is very mangled. So we, we kind of eat our own engineers, always like to play. They, that's still full programming. It's not just class file, the class file. No, no, no, it's a full optimization pipeline. Just there's a few things that we can do but X because we know, decks has slightly different semantics. For instance, there are some of the limitations that the, the DBA may have proposed on, especially on some constructor patterns that the art run time does not, which we can take advantage of. So we can do even more on a decks back end, but the class file back end is has almost all the optimization and the decks extra excited because we have so much compost code now. And even then, I write it there. So lamb is in properties. But be careful with compost for desktop because in my experience, you can optimize compost for desktop, but it's missing a lot of rules out of the box. And the missing ones tend to change over versions of compost desktop. So, you know, you're running to the prime that we are describing at the beginning. I get to debug some of the debugging tools. So let's talk about that. Okay. So let's say, what are we doing around debugging crashes when the app has been optimized with our eight? So I mean, the first thing that sort of mentioned earlier and that we talked a little bit about was the D, the demangling of those crashed, those stack traces. But also one of the big things is that we just have a lot more documentation now of what is the problem pattern that you see and how do you write a key role to protect yourself against that because you were being naughty and using reflection. Yeah, but when you're when you're running to one of those what the exception you get at runtime is like, you know, your reflection call will fail or it will try to call method that doesn't exist. So that is not sophisticated. There was a proposal, right, that you would leave in a little bit of extra, you know, and give a more usable trail that something can't be deleted. So instead of, you know, no such method found exception. So what what we have been or are working on at the moment is actually to instrument the code so that you can if you have something that uses reflection, then at every call site that has reflection has a reflection, will actually instrument the code to capture what reflection happened at runtime, which means that if you have an app that has been instrumented that way, then it will just track every single reflective call. You'll get all the various arguments that were passed to the reflective calls. And then you can run all your tests given that you have a very good test coverage. And if you like have a crash and you don't have test coverage for us, then you can start by making a new test that actually can reproduce a crash. And then you can run in the instrumented in the instrument version. And then you can then take all the effects that have happened, and then you can compare it against your key rules, and make sure that then they can find what's missing. - So this is like a debug mode of R8. This is not in production, we will do something to make crashes more. This is bringing your release only crashes into your debug mode so that you don't have to do a full R8 release so that you can find them also in tests. - Yeah, and you can have your CISO or-- - It's so coming. - This is the reflection profile. (laughing) - Yeah, but it's an interesting problem because there's also reflection in there that is actually supposed to be there, and sometimes even supposed to fail at runtime because you can use Reflexive for feature checks. So it's not just an error that a reflective call is not finding what it's looking for, it might be the intent. So yeah, that's an interesting problem in the area. - And then you've been doing something about annotations, right, like we had a key annotation a long time ago which was sort of frowned upon a little bit. - Yeah, it was very easy, but the problem was that it was sitting with the code that was being deleted and that's not really the problem. You want to sort of keep track of the usage side instead, right? - Well, so the key annotation today has a number of problems. Basically what the key annotation does is it does something that you can do in libraries today where you say, I have an annotation and then you reference that annotation from key rules that are distributed in the consumer rules of that library that say, hey, when this is applied to a class, do this. When this is applied to a field or method, do this. Unfortunately, the way that that works today is that it is when applied to a class completely unconditional and will keep every property of that class. When it is applied to a field or method, it will keep those if the outer class is kept and it will prevent them from being renamed. But there's not a lot of flexibility there. And exactly as Tor said, that is with the receiver of the reflection, not with the reflection. - So if that class becomes unused, you still wouldn't know, right? - Yes. - You wouldn't know. - Or it's gonna go, we're supposed to keep it even though, absolutely. It's no longer needed. So the big thing that we've been working on is to try and overhaul this entire idea of what if we could move things out of key rules into the code, but and exactly to your point, move them to the reflection call sites rather than the receiver call sites. And this has a number of different advantages. So when you put this on the reflection call site, then you're able to understand that this is coming from this function call into another one, which allows you to write a conditional key rule. Conditional key rules are where when you put a dash if at the beginning and you state, if this class or method is present, then keep these other things, which allows you to say, have an optional portion of your library reflect into something that doesn't force that thing to always be kept even if no one's using that optional section of your library. So what we're doing is we are, we came up with a key annotation that allows you to specify a bunch of different properties of that receiver call and then built a literal to automatically generate that functionally a key rule, but an annotation form for any reflection call sites. And so right now we're actively testing this out. What I really like about it is is not actually a key annotation, it's a use this reflection annotation. Yes. So it isn't saying this is going to keep it saying, hey, this is reflecting to this other thing. Yes. That has potential other uses. Yeah, it does. I mean, so if you do find use it, it's in the ID theoretically. We should now also flag the reflective use it is right? By just looking for these annotations. That's one example, right? So it's just, I love that this is what annotations are supposed to be metadata. Yes. So like, so the primary APIs here are uses reflection to construct, uses reflection to access field method, that kind of thing. And they allow you to find the common patterns where you reflect into something and you refer to it. And the great thing is I don't have to figure out the key annotation syntax every time that I want to write one of these things. Because like, you know, the IDE will do some decent job of giving me reds, quiggles when I get it completely wrong. But it is much better to have something where I can just pass in class reference. It's very simple. Which I have access to them. And it lives with the code. So when you move it around, the annotation moves. So it is better all around. You can refactor it. And it stays where it needs to be. Now can we have the same thing for Jane? Or I can put that annotation in the C++ code? I mean, so actually you could-- So this is one of the things that we talked about when naming this annotation. You can absolutely use this for JNI. If you know the Java entry point that calls into the JNI that has an up call. So right. But I would like the annotation to be-- In the native code? In the native code where I'm using the-- We would have to teach our read about. Well, it's basically the same thing. You just have to handle it on the C side. So you have to have the C++ compiler generate-- have these annotations or metadata on the C code, and then spin it out. When is our eight? Can it start compiling C code? When you cross the compiler-- Feature request. Yeah. So we actually had a request of putting information about what native code reference was left. So what system.load library calls was left in the code and what native methods were left. So that in principle, you could go and mark with yes or five afterwards. So bringing this back from things that we're going to do or thinking about, that actually brings us right over to optimize resource shrinking, which I think is one of the coolest things that our eight has done recently. Do you want to talk about that, Sara? Yeah, sure. So yeah, optimize resource shrinking is basically taking-- resources, of course, is a huge personal app. And usually some of the artifacts that are actually the biggest you have in the issue-- It's way bigger than code, right? Like the launcher icon. And sometimes people forget to do it in the right resolution. So they are way bigger than they should be. That aside, we used to have a resource shrinking be something that happened after a rate. So that meant that you had the AAPT tool, generate key rules for everything that was used in your view model and your R classes. And then afterwards, we'll then run out the code and move that. Now what is used reflection to access the resources? Yeah. Then you need a key rule. We actually had that. Yeah, there is a key rule. There is a key rule. And in the XML, so they can say that this is used by reflection. If you do look up-- About a third of the reflection in AndroidX libraries is exactly for that. Referencing something that is in some sort of manifest or key rule. And then, inflating that data. But going back to optimize resource shrinking. So what that basically means is that instead of the resource shrinking and the code shrinking being two different things, then they are one thing now. So that makes it mean that our rate receives the code and it receives all the resources. So in principle, all your resource XML files, which means that they can then do its walking and finding in live code across the two things. So it basically starts from the manifest, which kind of have all the rules, all the entry points into the program. And then it goes from where it works through the code. They find out what views are inflated, what's found in the resources. Code to resources resources to code, and instead of having those two be split, which can actually make quite a bit of savings. Because there's just a lot of things that, especially when you take libraries, they have activities for all kinds of fancy stuff that you don't use. And in the old world, they would not have. Not even just that, but you're rooting this C-Online when people look at the content of APKs, they find files that have been left that are never meant to even be a resource. And some of those can be pretty big. So I have a horror story for you. So Tor here is wearing this beautiful smart watch on his left hand. It's a Pixel 3. That's right. And it has a gorgeous circular display. And a few years ago, I did some work with the team that ships the software on these devices. And they needed me to help them understand what are the different things that are kept in memory to keep this device small, keep the battery lasting all day and so on. And one of my first discovery is in the memory on this device, I found several copies of a phone-sized wallpaper image. Probably taken by Roman, I'm guessing. It's possible. Could have been. A lot of those photographs are sourced from Roman's growing camera roll of beautiful shots. Yes, so it was sitting there on the watch. And it was there because the watch was using some default assets from the Android framework. And those assets had some kind of default wallpaper because you would need something. And what just did not exist when we wrote that code? That's true. Many things. So Android now ships on so many devices. And even if you just constrained to a single form factor, where you say, OK, just phones. Just phones. There's no such thing as just phones because there is so much variation in terms of capabilities, which is why we also use a lot, not just in apps, but also in the platform itself. So one example for that is a lot of times we will develop new features in the platform for the introduction of some new hardware capability. So these days, if you buy an expensive phone, probably comes with some satellite connectivity capabilities. So if you're in an emergency and there's no other connection, you can still call for help and you can send your location and emergency services, hopefully, we'll find you. And so there is a lot of code to run that modem and make it work and also present you with the UI for how to find the satellite connection and what message to compose and what have you. And that code is completely dead and unnecessary devices that just don't have. the hardware to send a satellite signal. So we don't ship that code. There's a lot of checks in the Android platform build that check for what is the target device capability. You as a handset maker, not an app developer, are making a handset and it has certain things and doesn't have other capabilities. What code is conditional on those features being available? Another example is there's features that are built just for VR. There's features that are built just for television displays and there's no reason to ship those features to watch or to an automotive head unit or what have you. So is is are a capable of doing shrinking based on Android feature flags? Yes, but you have to pay attention to how you generate the code for the feature flags. So we had done a lot of work on this and that maybe this is important for anyone listening who's working on a system for a built-on feature flagging or build time build flavor targeting where we made sure that at the time that r8 runs in the program that r8 inspects, it has all the information it needs for your release bill to make a simple determination for is this flag true or false in product? Does it choose not a runtime thing? Does it choose the assume value that Chris mentioned earlier? It ultimately bottoms out and assumes value. And importantly, you are going to want to have some builds of your software that do not assume the value and actually give you a runtime toggle. So being on the Android platform team, I care on the phone that has the latest and greatest features and many of those features are behind a flag and on my phone they're enabled and if my phone starts crashing or is super warm, I can disable them. And so the code for those features is available and the toggle is at runtime because that is-- The toggle does turn off crashes. Yeah, I turn off the crashes right away as soon as I get to phone other blocks. I don't know why. That's a feature we should put in every phone. Stop crashing. I feel like we could talk all day. What have we not asked you about? I think we'd like to talk about just maybe the primary few things that the average app developer should be thinking about when they interact with our eight where they think about optimization. So the first thing that I would say is turn on our eight. If you need to, you can use the package scope in order to limit it to a subset of your application but slowly march down that list of package-wide keyp rules or packages that you're not including in that package scope and add them because every single one of those big packages that you optimize, it'll reduce your A&R rate. It'll make your startups faster and all of those have like actual measurable business impact. I really want to stress out again because I don't think we can stress it out enough but just do turn on our eight like the gains are so massive and having these kind of gains that we manual optimization would take years of efforts. At least take that. Again, we should just turn it on for people but we don't necessarily want people to have access to them. Sure, that's fine. We've been holding back but really, we want-- What I'm getting at is that you know it might be a bit of work but it's nothing compared to what you get. Yeah, and also that's like historically we had different modes of our eight so that was a compact mode because we didn't want to turn on all the optimization we had to begin with because that was I guess the developer was not ready for them so we had a compact mode and it's very easy to forget to turn that on so you get all the optimization. So there's the-- Make sure you use our rate, make sure you're not using compact mode, make sure you're not using the wrong program configuration file from from HTTP so that you get full optimization. I guess that's the kind of steps you have to take. Yeah, and all of those things are listed out at d.edred.com/r8. So I can give your listeners something maybe more substantial to think about in terms of what does this mean to get these performance uplifts? What does this actually mean for me as a user? So we've done a lot of work with certain app developers top app developers on our adoption and getting the most out of our eight and in our studies generally speaking one of the things we do is we compare your apps performance in critical user journeys like startup or other important features in the app. We see how long does it take you to get into something and we compare the whole thing before sort of a before and after shot and because we have data from different devices we can also cast that comparison not in terms of you got a software upgrade as a user but you got a hardware upgrade as a user. So typically when we work with a big app developer and they go from zero to hero on our adoption we see the kind of performance implication that is as if their user updated to a new device that is four years newer. And yes, between over three sometimes four three and a half I would say and we can draw those comparisons because we know how long it takes your app to launch on this you know 2026 device and we know how long it takes on this 2022 device and we can say your old software on the 2022 device performs sorry your new software on the old 2022 device performs just like your old software on the 2020 65s it's like you've given your users a new phone or tablet or whatever the device they are using for free. And that kind of stat we've seen repeatedly. Yes, consistent consistent three four year uplift in performance on the same series of device. I mean it's a pretty pretty compelling reason to go try it now. Soren that's a lot of three phones you've given away. Well that's so generous. Yeah that sounds good. Maybe it's actually what's going on and so on actually giving new phones to people. I suppose we bury the lead maybe we should have opened with art is a tool that makes your phone four years younger. Here's how he works. Face lift for phones. That's right. Well thank you so much. Thank you. Thank you so much. [BLANK_AUDIO]

Podcast Summary

Key Points:

  1. R8 is an optimizing compiler for Android that performs whole-program analysis to remove dead code and apply performance optimizations during release builds.
  2. D8 is a faster, incremental compiler used for day-to-day debugging and development, focusing on speed over optimization.
  3. R8's optimizations, such as inlining and tree-shaking, can significantly improve app performance and enable better on-device compilation by ART.
  4. Reflection and JNI usage can interfere with R8's static analysis, requiring "Keep Rules" to preserve necessary code, but overly broad rules can hinder optimization.
  5. R8 organizes code into DEX files with attention to locality (e.g., startup code in the first DEX file) to enhance runtime performance and memory efficiency.

Summary:

R8 is a whole-program optimizing compiler for Android, used in release builds to remove dead code and apply advanced optimizations like inlining and method merging, which improve app performance and prepare code for efficient on-device compilation by ART. In contrast, D8 serves as a fast, incremental debug compiler for development. R8's static analysis can be challenged by reflection or JNI, requiring "Keep Rules" to preserve necessary code, though overly broad rules from libraries may limit optimizations.

The compiler also strategically organizes code into DEX files, prioritizing startup code and maintaining package locality to enhance memory performance. While many apps use R8, its adoption could increase with better tooling and awareness of its benefits for performance and size reduction.

FAQs

R8 is a fully optimizing compiler used for release builds that applies whole program optimizations like dead code removal. D8 is a debug compiler for day-to-day development, focusing on fast incremental compilation without optimizations.

D8 enables quick development cycles by compiling only changed code for fast deployment. R8 is used for release builds to spend time on whole program optimizations, which require analyzing the entire app for performance improvements.

In your build.gradle or build.gradle.kts file, set minifyEnabled to true for release builds. Alternatively, use the newer DSL option 'optimization = true' available in AGP 9 for a more intuitive way to turn on optimizations.

Keep Rules are instructions that tell R8 to preserve specific classes, methods, or fields that are accessed via reflection or JNI, which R8 might otherwise remove as dead code. They are essential for libraries or code using dynamic features.

R8 optimizes code by inlining methods, merging interfaces with single implementations, and removing virtual dispatches when possible. It also works with profiles to prioritize hot code, improving runtime performance and aiding on-device compilation.

Reflection and JNI can cause R8 to incorrectly remove code that is used dynamically at runtime, leading to ClassNotFoundException or runtime errors. Proper Keep Rules must be applied to prevent this, especially for libraries using these features.

Chat with AI

Loading...

Pro features

Go deeper with this episode

Unlock creator-grade tools that turn any transcript into show notes and subtitle files.